From 5070941dc4056cee3eb93713752b77d3684fc0ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Friedrich=20Dreyer?= Date: Fri, 9 Jul 2021 15:15:20 +0000 Subject: [PATCH 1/3] graph: refactor auth and middlewares MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jörn Friedrich Dreyer --- graph/pkg/config/config.go | 6 ++ graph/pkg/flagset/flagset.go | 7 ++ graph/pkg/middleware/auth.go | 78 +++++++++++++++++++++ graph/pkg/server/http/server.go | 16 ++--- graph/pkg/service/v0/drives.go | 39 ----------- graph/pkg/service/v0/errorcode/errorcode.go | 8 ++- graph/pkg/service/v0/groups.go | 8 +-- graph/pkg/service/v0/users.go | 51 +++++++++----- 8 files changed, 141 insertions(+), 72 deletions(-) create mode 100644 graph/pkg/middleware/auth.go diff --git a/graph/pkg/config/config.go b/graph/pkg/config/config.go index 0e628bcd7..ca1a5f934 100644 --- a/graph/pkg/config/config.go +++ b/graph/pkg/config/config.go @@ -63,6 +63,11 @@ type Reva struct { Address string } +// TokenManager is the config for using the reva token manager +type TokenManager struct { + JWTSecret string +} + type Spaces struct { WebDavBase string } @@ -79,6 +84,7 @@ type Config struct { Ldap Ldap OpenIDConnect OpenIDConnect Reva Reva + TokenManager TokenManager Spaces Spaces Context context.Context diff --git a/graph/pkg/flagset/flagset.go b/graph/pkg/flagset/flagset.go index f53b0788d..44028fd8b 100644 --- a/graph/pkg/flagset/flagset.go +++ b/graph/pkg/flagset/flagset.go @@ -213,6 +213,13 @@ func ServerWithConfig(cfg *config.Config) []cli.Flag { Destination: &cfg.OpenIDConnect.Realm, }, + &cli.StringFlag{ + Name: "jwt-secret", + Value: flags.OverrideDefaultString(cfg.TokenManager.JWTSecret, "Pive-Fumkiu4"), + Usage: "Used to validate the reva access JWT, should equal reva's jwt-secret", + EnvVars: []string{"GRAPH_JWT_SECRET", "OCIS_JWT_SECRET"}, + Destination: &cfg.TokenManager.JWTSecret, + }, &cli.StringFlag{ Name: "reva-gateway-addr", Value: flags.OverrideDefaultString(cfg.Reva.Address, "127.0.0.1:9142"), diff --git a/graph/pkg/middleware/auth.go b/graph/pkg/middleware/auth.go new file mode 100644 index 000000000..1436f16fd --- /dev/null +++ b/graph/pkg/middleware/auth.go @@ -0,0 +1,78 @@ +package middleware + +import ( + "net/http" + + "github.com/cs3org/reva/pkg/auth/scope" + "github.com/cs3org/reva/pkg/token" + "github.com/cs3org/reva/pkg/token/manager/jwt" + "github.com/cs3org/reva/pkg/user" + "github.com/owncloud/ocis/graph/pkg/service/v0/errorcode" + "github.com/owncloud/ocis/ocis-pkg/account" + "google.golang.org/grpc/metadata" +) + +// authOptions initializes the available default options. +func authOptions(opts ...account.Option) account.Options { + opt := account.Options{} + + for _, o := range opts { + o(&opt) + } + + return opt +} + +// Auth provides a middleware to authenticate requestrs using the x-access-token header value +// and write it to the context. If there is no x-access-token the middleware prevents access and renders a json document. +func Auth(opts ...account.Option) func(http.Handler) http.Handler { + opt := authOptions(opts...) + tokenManager, err := jwt.New(map[string]interface{}{ + "secret": opt.JWTSecret, + "expires": int64(60), + }) + if err != nil { + opt.Logger.Fatal().Err(err).Msgf("Could not initialize token-manager") + } + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + t := r.Header.Get("x-access-token") + if t == "" { + errorcode.InvalidAuthenticationToken.Render(w, r, http.StatusUnauthorized, "Access token is empty.") + /* msgraph error for GET https://graph.microsoft.com/v1.0/me + { + "error": + { + "code":"InvalidAuthenticationToken", + "message":"Access token is empty.", + "innerError":{ + "date":"2021-07-09T14:40:51", + "request-id":"bb12f7db-b4c4-43a9-ba4b-31676aeed019", + "client-request-id":"bb12f7db-b4c4-43a9-ba4b-31676aeed019" + } + } + } + */ + return + } + + u, tokenScope, err := tokenManager.DismantleToken(r.Context(), t) + if err != nil { + errorcode.InvalidAuthenticationToken.Render(w, r, http.StatusUnauthorized, "invalid token") + return + } + if ok, err := scope.VerifyScope(tokenScope, r); err != nil || !ok { + opt.Logger.Error().Err(err).Msg("verifying scope failed") + errorcode.InvalidAuthenticationToken.Render(w, r, http.StatusUnauthorized, "verifying scope failed") + return + } + + ctx = token.ContextSetToken(ctx, t) + ctx = user.ContextSetUser(ctx, u) + ctx = metadata.AppendToOutgoingContext(ctx, token.TokenHeader, t) + + next.ServeHTTP(w, r.WithContext(ctx)) + }) + } +} diff --git a/graph/pkg/server/http/server.go b/graph/pkg/server/http/server.go index 60f2498b9..c9d733e65 100644 --- a/graph/pkg/server/http/server.go +++ b/graph/pkg/server/http/server.go @@ -2,10 +2,11 @@ package http import ( "github.com/asim/go-micro/v3" + graphMiddleware "github.com/owncloud/ocis/graph/pkg/middleware" svc "github.com/owncloud/ocis/graph/pkg/service/v0" "github.com/owncloud/ocis/graph/pkg/version" + "github.com/owncloud/ocis/ocis-pkg/account" "github.com/owncloud/ocis/ocis-pkg/middleware" - "github.com/owncloud/ocis/ocis-pkg/oidc" "github.com/owncloud/ocis/ocis-pkg/service/http" ) @@ -27,11 +28,6 @@ func Server(opts ...Option) (http.Service, error) { svc.Logger(options.Logger), svc.Config(options.Config), svc.Middleware( - middleware.RealIP, - middleware.RequestID, - middleware.NoCache, - middleware.Cors, - middleware.Secure, middleware.Version( "graph", version.String, @@ -39,11 +35,9 @@ func Server(opts ...Option) (http.Service, error) { middleware.Logger( options.Logger, ), - middleware.OpenIDConnect( - oidc.Endpoint(options.Config.OpenIDConnect.Endpoint), - oidc.Realm(options.Config.OpenIDConnect.Realm), - oidc.Insecure(options.Config.OpenIDConnect.Insecure), - oidc.Logger(options.Logger), + graphMiddleware.Auth( + account.Logger(options.Logger), + account.JWTSecret(options.Config.TokenManager.JWTSecret), ), ), ) diff --git a/graph/pkg/service/v0/drives.go b/graph/pkg/service/v0/drives.go index df8454587..3677da347 100644 --- a/graph/pkg/service/v0/drives.go +++ b/graph/pkg/service/v0/drives.go @@ -8,42 +8,17 @@ import ( "time" "github.com/go-chi/render" - "google.golang.org/grpc/metadata" cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" - "github.com/cs3org/reva/pkg/token" msgraph "github.com/owncloud/open-graph-api-go" ) -func getToken(r *http.Request) string { - // 1. check Authorization header - hdr := r.Header.Get("Authorization") - t := strings.TrimPrefix(hdr, "Bearer ") - if t != "" { - return t - } - // TODO 2. check form encoded body parameter for POST requests, see https://tools.ietf.org/html/rfc6750#section-2.2 - - // 3. check uri query parameter, see https://tools.ietf.org/html/rfc6750#section-2.3 - tokens, ok := r.URL.Query()["access_token"] - if !ok || len(tokens[0]) < 1 { - return "" - } - - return tokens[0] -} - // GetDrives implements the Service interface. func (g Graph) GetDrives(w http.ResponseWriter, r *http.Request) { g.logger.Info().Msg("Calling GetDrives") - if getToken(r) == "" { - g.logger.Error().Msg("no access token provided in request") - w.WriteHeader(http.StatusForbidden) - return - } ctx := r.Context() client, err := g.GetClient() @@ -52,9 +27,6 @@ func (g Graph) GetDrives(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) return } - t := r.Header.Get("x-access-token") - ctx = token.ContextSetToken(ctx, t) - ctx = metadata.AppendToOutgoingContext(ctx, "x-access-token", t) res, err := client.ListStorageSpaces(ctx, &storageprovider.ListStorageSpacesRequest{}) if err != nil { @@ -90,11 +62,6 @@ func (g Graph) GetDrives(w http.ResponseWriter, r *http.Request) { // GetRootDriveChildren implements the Service interface. func (g Graph) GetRootDriveChildren(w http.ResponseWriter, r *http.Request) { g.logger.Info().Msg("Calling GetRootDriveChildren") - if getToken(r) == "" { - g.logger.Error().Msg("no access token provided in request") - w.WriteHeader(http.StatusForbidden) - return - } ctx := r.Context() fn := g.config.WebdavNamespace @@ -106,12 +73,6 @@ func (g Graph) GetRootDriveChildren(w http.ResponseWriter, r *http.Request) { return } - t := r.Header.Get("x-access-token") - ctx = token.ContextSetToken(ctx, t) - ctx = metadata.AppendToOutgoingContext(ctx, "x-access-token", t) - - g.logger.Info().Interface("context", ctx).Msg("provides access token") - ref := &storageprovider.Reference{ Path: fn, } diff --git a/graph/pkg/service/v0/errorcode/errorcode.go b/graph/pkg/service/v0/errorcode/errorcode.go index 6567d0212..c2d8805bf 100644 --- a/graph/pkg/service/v0/errorcode/errorcode.go +++ b/graph/pkg/service/v0/errorcode/errorcode.go @@ -17,6 +17,8 @@ const ( ActivityLimitReached // GeneralException defines the error if an unspecified error has occurred. GeneralException + // InvalidAuthenticationToken defines the error if the access token is missing + InvalidAuthenticationToken // InvalidRange defines the error if the specified byte range is invalid or unavailable. InvalidRange // InvalidRequest defines the error if the request is malformed or incorrect. @@ -47,6 +49,7 @@ var errorCodes = [...]string{ "accessDenied", "activityLimitReached", "generalException", + "InvalidAuthenticationToken", "invalidRange", "invalidRequest", "itemNotFound", @@ -62,9 +65,10 @@ var errorCodes = [...]string{ } // Render writes an Graph ErrorObject to the response writer -func (e ErrorCode) Render(w http.ResponseWriter, r *http.Request, status int) { +func (e ErrorCode) Render(w http.ResponseWriter, r *http.Request, status int, msg string) { resp := &msgraph.ErrorObject{ - Code: e.String(), + Code: e.String(), + Message: msg, } render.Status(r, status) render.JSON(w, r, resp) diff --git a/graph/pkg/service/v0/groups.go b/graph/pkg/service/v0/groups.go index c98619c21..2d0277da5 100644 --- a/graph/pkg/service/v0/groups.go +++ b/graph/pkg/service/v0/groups.go @@ -20,7 +20,7 @@ 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) + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "groupID empty") return } // TODO make filter configurable @@ -28,7 +28,7 @@ func (g Graph) GroupCtx(next http.Handler) http.Handler { group, err := g.ldapGetSingleEntry(g.config.Ldap.BaseDNGroups, filter) if err != nil { g.logger.Info().Err(err).Msgf("Failed to read group %s", groupID) - errorcode.ItemNotFound.Render(w, r, http.StatusNotFound) + errorcode.ItemNotFound.Render(w, r, http.StatusInternalServerError, "") return } @@ -42,7 +42,7 @@ func (g Graph) GetGroups(w http.ResponseWriter, r *http.Request) { con, err := g.initLdap() if err != nil { g.logger.Error().Err(err).Msg("Failed to initialize ldap") - errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError) + errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, "") return } @@ -51,7 +51,7 @@ func (g Graph) GetGroups(w http.ResponseWriter, r *http.Request) { if err != nil { g.logger.Error().Err(err).Msg("Failed search ldap with filter: '(objectClass=posixGroup)'") - errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError) + errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, "") return } diff --git a/graph/pkg/service/v0/users.go b/graph/pkg/service/v0/users.go index ba20972b2..a18eec6ea 100644 --- a/graph/pkg/service/v0/users.go +++ b/graph/pkg/service/v0/users.go @@ -5,18 +5,19 @@ import ( "fmt" "net/http" - "github.com/owncloud/ocis/graph/pkg/service/v0/errorcode" - + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + "github.com/cs3org/reva/pkg/user" "github.com/go-chi/chi" "github.com/go-chi/render" "github.com/go-ldap/ldap/v3" - "github.com/owncloud/ocis/ocis-pkg/oidc" + "github.com/owncloud/ocis/graph/pkg/service/v0/errorcode" 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) { var user *ldap.Entry @@ -24,7 +25,7 @@ func (g Graph) UserCtx(next http.Handler) http.Handler { userID := chi.URLParam(r, "userID") if userID == "" { - errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest) + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "") return } // TODO make filter configurable @@ -32,7 +33,7 @@ func (g Graph) UserCtx(next http.Handler) http.Handler { user, err = g.ldapGetSingleEntry(g.config.Ldap.BaseDNUsers, filter) if err != nil { g.logger.Info().Err(err).Msgf("Failed to read user %s", userID) - errorcode.ItemNotFound.Render(w, r, http.StatusNotFound) + errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, "") return } @@ -43,30 +44,48 @@ func (g Graph) UserCtx(next http.Handler) http.Handler { // GetMe implements the Service interface. func (g Graph) GetMe(w http.ResponseWriter, r *http.Request) { - claims := oidc.FromContext(r.Context()) - g.logger.Info().Interface("Claims", claims).Msg("Claims in /me") - // TODO make filter configurable - filter := fmt.Sprintf("(&(objectClass=posixAccount)(cn=%s))", claims.PreferredUsername) - user, err := g.ldapGetSingleEntry(g.config.Ldap.BaseDNUsers, filter) - if err != nil { - g.logger.Info().Err(err).Msgf("Failed to read user %s", claims.PreferredUsername) - errorcode.ItemNotFound.Render(w, r, http.StatusNotFound) + u, ok := user.ContextGetUser(r.Context()) + if !ok { + errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, "") return } - me := createUserModelFromLDAP(user) + g.logger.Info().Interface("user", u).Msg("User in /me") + + me := createUserModelFromCS3(u) render.Status(r, http.StatusOK) render.JSON(w, r, me) } +func createUserModelFromCS3(u *userpb.User) *msgraph.User { + return &msgraph.User{ + DisplayName: &u.DisplayName, + Mail: &u.Mail, + // TODO u.Groups + 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, + }, + }, + }, + }, + } +} + // GetUsers implements the Service interface. +// TODO use cs3 api to look up user func (g Graph) GetUsers(w http.ResponseWriter, r *http.Request) { con, err := g.initLdap() if err != nil { g.logger.Error().Err(err).Msg("Failed to initialize ldap") - errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError) + errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, "") return } @@ -75,7 +94,7 @@ func (g Graph) GetUsers(w http.ResponseWriter, r *http.Request) { if err != nil { g.logger.Error().Err(err).Msg("Failed search ldap with filter: '(objectClass=posixAccount)'") - errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError) + errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, "") return } From 2a54b647f101f6e9bf249f5dff4d1b2ac0f03a07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Friedrich=20Dreyer?= Date: Fri, 9 Jul 2021 22:20:30 +0000 Subject: [PATCH 2/3] get rid of ldap and oidc, refactor error handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jörn Friedrich Dreyer --- graph/go.mod | 2 +- graph/go.sum | 4 - graph/pkg/config/config.go | 39 ++--- graph/pkg/flagset/flagset.go | 73 +--------- graph/pkg/server/http/server.go | 1 + graph/pkg/service/v0/drives.go | 102 ++++++++----- graph/pkg/service/v0/errorcode/errorcode.go | 20 ++- graph/pkg/service/v0/graph.go | 6 +- graph/pkg/service/v0/groups.go | 110 ++++++++++---- graph/pkg/service/v0/ldap.go | 94 ------------ graph/pkg/service/v0/users.go | 154 ++++++++++++-------- 11 files changed, 269 insertions(+), 336 deletions(-) delete mode 100644 graph/pkg/service/v0/ldap.go diff --git a/graph/go.mod b/graph/go.mod index ac33ce4eb..952ae74b9 100644 --- a/graph/go.mod +++ b/graph/go.mod @@ -6,12 +6,12 @@ require ( contrib.go.opencensus.io/exporter/jaeger v0.2.1 contrib.go.opencensus.io/exporter/ocagent v0.7.0 contrib.go.opencensus.io/exporter/zipkin v0.1.2 + github.com/ascarter/requestid v0.0.0-20170313220838-5b76ab3d4aee github.com/asim/go-micro/v3 v3.5.1-0.20210217182006-0f0ace1a44a9 github.com/cs3org/go-cs3apis v0.0.0-20210702091910-85a56bfd027f github.com/cs3org/reva v1.10.0 github.com/go-chi/chi v4.1.2+incompatible github.com/go-chi/render v1.0.1 - github.com/go-ldap/ldap/v3 v3.3.0 github.com/micro/cli/v2 v2.1.2 github.com/oklog/run v1.1.0 github.com/openzipkin/zipkin-go v0.2.5 diff --git a/graph/go.sum b/graph/go.sum index 1bb29729a..c2f618890 100644 --- a/graph/go.sum +++ b/graph/go.sum @@ -305,7 +305,6 @@ github.com/cs3org/reva v1.6.1-0.20210329145723-ed244aac4ddc/go.mod h1:exwJqEJ8lV github.com/cs3org/reva v1.10.0 h1:8sne7z4pe+9rkGP3ZX2i3Sx1FMQ8hhBs5QCb4VVvZJI= github.com/cs3org/reva v1.10.0/go.mod h1:4bpcovnx3EAetafPIp4Fia1GkFvjFDkztacmCWI7cN0= github.com/cucumber/godog v0.8.1/go.mod h1:vSh3r/lM+psC1BPXvdkSEuNjmXfpVqrMGYAElF6hxnA= -github.com/cznic/b v0.0.0-20181122101859-a26611c4d92d/go.mod h1:URriBxXwVq5ijiJ12C7iIZqlA69nTlI+LgI6/pwftG8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -917,7 +916,6 @@ github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ= github.com/jmoiron/sqlx v0.0.0-20180614180643-0dae4fefe7c0/go.mod h1:IiEW3SEiiErVyFdH8NTuWjSifiEQKUoyK3LNqr2kCHU= github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak= @@ -1540,7 +1538,6 @@ github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69 github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= -github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c/go.mod h1:ahpPrc7HpcfEWDQRZEmnXMzHY03mLDYMCxeDzy46i+8= github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog= github.com/thejerf/suture/v4 v4.0.0 h1:GX3X+1Qaewtj9flL2wgoTBfLA5NcmrCY39TJRpPbUrI= github.com/thejerf/suture/v4 v4.0.0/go.mod h1:g0e8vwskm9tI0jRjxrnA6lSr0q6OfPdWJVX7G5bVWRs= @@ -2199,7 +2196,6 @@ google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG google.golang.org/grpc v1.39.0 h1:Klz8I9kdtkIN6EpHHUOMLCYhTn/2WAe5a0s1hcBkdTI= google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.0.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= -google.golang.org/grpc/examples v0.0.0-20210712234202-ebfe3be62a82/go.mod h1:bF8wuZSAZTcbF7ZPKrDI/qY52toTP/yxLpRRY4Eu9Js= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/graph/pkg/config/config.go b/graph/pkg/config/config.go index ca1a5f934..e678250f5 100644 --- a/graph/pkg/config/config.go +++ b/graph/pkg/config/config.go @@ -40,24 +40,6 @@ type Tracing struct { Service string } -// Ldap defined the available LDAP configuration. -type Ldap struct { - Network string - Address string - UserName string - Password string - BaseDNUsers string - BaseDNGroups string -} - -// OpenIDConnect defined the available OpenID Connect configuration. -type OpenIDConnect struct { - Endpoint string - Realm string - SigningAlgs []string - Insecure bool -} - // Reva defines all available REVA configuration. type Reva struct { Address string @@ -74,18 +56,15 @@ type Spaces struct { // Config combines all available configuration parts. type Config struct { - File string - WebdavNamespace string - Log Log - Debug Debug - HTTP HTTP - Server Server - Tracing Tracing - Ldap Ldap - OpenIDConnect OpenIDConnect - Reva Reva - TokenManager TokenManager - Spaces Spaces + File string + Log Log + Debug Debug + HTTP HTTP + Server Server + Tracing Tracing + Reva Reva + TokenManager TokenManager + Spaces Spaces Context context.Context Supervised bool diff --git a/graph/pkg/flagset/flagset.go b/graph/pkg/flagset/flagset.go index 44028fd8b..0481669ee 100644 --- a/graph/pkg/flagset/flagset.go +++ b/graph/pkg/flagset/flagset.go @@ -144,75 +144,11 @@ func ServerWithConfig(cfg *config.Config) []cli.Flag { &cli.StringFlag{ Name: "spaces-webdav-base", Value: flags.OverrideDefaultString(cfg.Spaces.WebDavBase, "https://localhost:9200/dav/spaces/"), - Usage: "spaces webdav base URL", + Usage: "spaces webdav base URL to use when rendering drive WabDAV URLs", EnvVars: []string{"GRAPH_SPACES_WEBDAV_BASE"}, Destination: &cfg.Spaces.WebDavBase, }, - &cli.StringFlag{ - Name: "ldap-network", - Value: flags.OverrideDefaultString(cfg.Ldap.Network, "tcp"), - Usage: "Network protocol to use to connect to the Ldap server", - EnvVars: []string{"GRAPH_LDAP_NETWORK"}, - Destination: &cfg.Ldap.Network, - }, - &cli.StringFlag{ - Name: "ldap-address", - Value: flags.OverrideDefaultString(cfg.Ldap.Address, "0.0.0.0:9125"), - Usage: "Address to connect to the Ldap server", - EnvVars: []string{"GRAPH_LDAP_ADDRESS"}, - Destination: &cfg.Ldap.Address, - }, - &cli.StringFlag{ - Name: "ldap-username", - Value: flags.OverrideDefaultString(cfg.Ldap.UserName, "cn=admin,dc=example,dc=org"), - Usage: "User to bind to the Ldap server", - EnvVars: []string{"GRAPH_LDAP_USERNAME"}, - Destination: &cfg.Ldap.UserName, - }, - &cli.StringFlag{ - Name: "ldap-password", - Value: flags.OverrideDefaultString(cfg.Ldap.Password, "admin"), - Usage: "Password to bind to the Ldap server", - EnvVars: []string{"GRAPH_LDAP_PASSWORD"}, - Destination: &cfg.Ldap.Password, - }, - &cli.StringFlag{ - Name: "ldap-basedn-users", - Value: flags.OverrideDefaultString(cfg.Ldap.BaseDNUsers, "ou=users,dc=example,dc=org"), - Usage: "BaseDN to look for users", - EnvVars: []string{"GRAPH_LDAP_BASEDN_USERS"}, - Destination: &cfg.Ldap.BaseDNUsers, - }, - &cli.StringFlag{ - Name: "ldap-basedn-groups", - Value: flags.OverrideDefaultString(cfg.Ldap.BaseDNGroups, "ou=groups,dc=example,dc=org"), - Usage: "BaseDN to look for users", - EnvVars: []string{"GRAPH_LDAP_BASEDN_GROUPS"}, - Destination: &cfg.Ldap.BaseDNGroups, - }, - - &cli.StringFlag{ - Name: "oidc-endpoint", - Value: flags.OverrideDefaultString(cfg.OpenIDConnect.Endpoint, "https://localhost:9200"), - Usage: "OpenIDConnect endpoint", - EnvVars: []string{"GRAPH_OIDC_ENDPOINT", "OCIS_URL"}, - Destination: &cfg.OpenIDConnect.Endpoint, - }, - &cli.BoolFlag{ - Name: "oidc-insecure", - Usage: "OpenIDConnect endpoint", - EnvVars: []string{"GRAPH_OIDC_INSECURE"}, - Destination: &cfg.OpenIDConnect.Insecure, - }, - &cli.StringFlag{ - Name: "oidc-realm", - Value: flags.OverrideDefaultString(cfg.OpenIDConnect.Realm, ""), - Usage: "OpenIDConnect realm", - EnvVars: []string{"GRAPH_OIDC_REALM"}, - Destination: &cfg.OpenIDConnect.Realm, - }, - &cli.StringFlag{ Name: "jwt-secret", Value: flags.OverrideDefaultString(cfg.TokenManager.JWTSecret, "Pive-Fumkiu4"), @@ -227,13 +163,6 @@ func ServerWithConfig(cfg *config.Config) []cli.Flag { EnvVars: []string{"REVA_GATEWAY_ADDR"}, Destination: &cfg.Reva.Address, }, - &cli.StringFlag{ - Name: "webdav-namespace", - Value: flags.OverrideDefaultString(cfg.WebdavNamespace, "/home"), - Usage: "Namespace prefix for the webdav endpoint", - EnvVars: []string{"STORAGE_WEBDAV_NAMESPACE"}, - Destination: &cfg.WebdavNamespace, - }, &cli.StringFlag{ Name: "extensions", Usage: "Run specific extensions during supervised mode. This flag is set by the runtime", diff --git a/graph/pkg/server/http/server.go b/graph/pkg/server/http/server.go index c9d733e65..0526ce0ef 100644 --- a/graph/pkg/server/http/server.go +++ b/graph/pkg/server/http/server.go @@ -28,6 +28,7 @@ func Server(opts ...Option) (http.Service, error) { svc.Logger(options.Logger), svc.Config(options.Config), svc.Middleware( + middleware.RequestID, middleware.Version( "graph", version.String, diff --git a/graph/pkg/service/v0/drives.go b/graph/pkg/service/v0/drives.go index 3677da347..9026796a8 100644 --- a/graph/pkg/service/v0/drives.go +++ b/graph/pkg/service/v0/drives.go @@ -4,15 +4,14 @@ import ( "math" "net/http" "net/url" - "strings" + "path" "time" - "github.com/go-chi/render" - cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" - + "github.com/go-chi/render" + "github.com/owncloud/ocis/graph/pkg/service/v0/errorcode" msgraph "github.com/owncloud/open-graph-api-go" ) @@ -24,34 +23,40 @@ func (g Graph) GetDrives(w http.ResponseWriter, r *http.Request) { client, err := g.GetClient() if err != nil { g.logger.Err(err).Msg("error getting grpc client") - w.WriteHeader(http.StatusInternalServerError) + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) return } - res, err := client.ListStorageSpaces(ctx, &storageprovider.ListStorageSpacesRequest{}) - if err != nil { + res, err := client.ListStorageSpaces(ctx, &storageprovider.ListStorageSpacesRequest{ + // TODO add filters? + }) + switch { + case err != nil: g.logger.Error().Err(err).Msg("error sending list storage spaces grpc request") - w.WriteHeader(http.StatusInternalServerError) - return - } - // TODO handle not found and other status codes - if res.Status.Code != cs3rpc.Code_CODE_OK { - g.logger.Error().Err(err).Interface("status", res.Status).Msg("error calling grpc list storage spaces") - w.WriteHeader(http.StatusInternalServerError) + 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 { + // return an empty list + render.Status(r, http.StatusOK) + render.JSON(w, r, &listResponse{}) + return + } + g.logger.Error().Err(err).Msg("error sending list storage spaces grpc request") + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, res.Status.Message) } wdu, err := url.Parse(g.config.Spaces.WebDavBase) if err != nil { g.logger.Error().Err(err).Msg("error parsing url") - w.WriteHeader(http.StatusInternalServerError) + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) return } files, err := formatDrives(wdu, res.StorageSpaces) if err != nil { g.logger.Error().Err(err).Msg("error encoding response as json") - w.WriteHeader(http.StatusInternalServerError) + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) return } @@ -64,39 +69,58 @@ func (g Graph) GetRootDriveChildren(w http.ResponseWriter, r *http.Request) { g.logger.Info().Msg("Calling GetRootDriveChildren") ctx := r.Context() - fn := g.config.WebdavNamespace - client, err := g.GetClient() if err != nil { - g.logger.Err(err).Msg("error getting grpc client") - w.WriteHeader(http.StatusInternalServerError) + g.logger.Error().Err(err).Msg("could not get client") + errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, err.Error()) return } - ref := &storageprovider.Reference{ - Path: fn, - } - - req := &storageprovider.ListContainerRequest{ - Ref: ref, - } - res, err := client.ListContainer(ctx, req) - if err != nil { - g.logger.Error().Err(err).Str("path", fn).Msg("error sending list container grpc request") - w.WriteHeader(http.StatusInternalServerError) + res, err := client.GetHome(ctx, &storageprovider.GetHomeRequest{}) + switch { + case err != nil: + g.logger.Error().Err(err).Msg("error sending get home grpc request") + errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, err.Error()) return - } - // TODO handle not found and other status codes - if res.Status.Code != cs3rpc.Code_CODE_OK { - g.logger.Error().Err(err).Str("path", fn).Interface("status", res.Status).Msg("error calling grpc list container") - w.WriteHeader(http.StatusInternalServerError) + 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).Msg("error sending get home grpc request") + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, res.Status.Message) return } - files, err := formatDriveItems(res.Infos) + lRes, err := client.ListContainer(ctx, &storageprovider.ListContainerRequest{ + Ref: &storageprovider.Reference{ + Path: res.Path, + }, + }) + switch { + case err != nil: + g.logger.Error().Err(err).Msg("error sending list container 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 + } + if res.Status.Code == cs3rpc.Code_CODE_PERMISSION_DENIED { + // TODO check if we should return 404 to not disclose existing items + errorcode.AccessDenied.Render(w, r, http.StatusForbidden, res.Status.Message) + return + } + g.logger.Error().Err(err).Msg("error sending list container grpc request") + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, res.Status.Message) + return + } + + files, err := formatDriveItems(lRes.Infos) if err != nil { g.logger.Error().Err(err).Msg("error encoding response as json") - w.WriteHeader(http.StatusInternalServerError) + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) return } @@ -111,7 +135,7 @@ func cs3TimestampToTime(t *types.Timestamp) time.Time { func cs3ResourceToDriveItem(res *storageprovider.ResourceInfo) (*msgraph.DriveItem, error) { size := new(int64) *size = int64(res.Size) // uint64 -> int :boom: - name := strings.TrimPrefix(res.Path, "/home/") + name := path.Base(res.Path) driveItem := &msgraph.DriveItem{ BaseItem: msgraph.BaseItem{ diff --git a/graph/pkg/service/v0/errorcode/errorcode.go b/graph/pkg/service/v0/errorcode/errorcode.go index c2d8805bf..068e68478 100644 --- a/graph/pkg/service/v0/errorcode/errorcode.go +++ b/graph/pkg/service/v0/errorcode/errorcode.go @@ -2,9 +2,11 @@ package errorcode import ( "net/http" + "time" "github.com/go-chi/render" - msgraph "github.com/yaegashi/msgraph.go/v1.0" + "github.com/ascarter/requestid" + msgraph "github.com/owncloud/open-graph-api-go" ) // 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 @@ -66,9 +68,19 @@ var errorCodes = [...]string{ // Render writes an Graph ErrorObject to the response writer func (e ErrorCode) Render(w http.ResponseWriter, r *http.Request, status int, msg string) { - resp := &msgraph.ErrorObject{ - Code: e.String(), - Message: msg, + innererror := map[string]interface{}{ + "date": time.Now().UTC().Format(time.RFC3339), + // TODO return client-request-id? + } + if id, ok := requestid.FromContext(r.Context()); ok { + innererror["request-id"] = id + } + resp := &msgraph.OdataError{ + Error: msgraph.OdataErrorMain{ + Code: e.String(), + Message: msg, + Innererror: &innererror, + }, } render.Status(r, status) render.JSON(w, r, resp) diff --git a/graph/pkg/service/v0/graph.go b/graph/pkg/service/v0/graph.go index ad6d65816..71bef8578 100644 --- a/graph/pkg/service/v0/graph.go +++ b/graph/pkg/service/v0/graph.go @@ -31,8 +31,10 @@ func (g Graph) GetClient() (gateway.GatewayAPIClient, error) { // other packages. type key int -const userIDKey key = 0 -const groupIDKey key = 1 +const ( + userKey key = iota + groupKey +) type listResponse struct { Value interface{} `json:"value,omitempty"` diff --git a/graph/pkg/service/v0/groups.go b/graph/pkg/service/v0/groups.go index 2d0277da5..c31ed3953 100644 --- a/graph/pkg/service/v0/groups.go +++ b/graph/pkg/service/v0/groups.go @@ -2,14 +2,16 @@ package svc import ( "context" - "fmt" "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" "github.com/go-chi/render" - "github.com/go-ldap/ldap/v3" + + //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" ) @@ -20,50 +22,80 @@ 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, "groupID empty") - return - } - // TODO make filter configurable - filter := fmt.Sprintf("(&(objectClass=posixGroup)(ownCloudUUID=%s))", groupID) - group, err := g.ldapGetSingleEntry(g.config.Ldap.BaseDNGroups, filter) - if err != nil { - g.logger.Info().Err(err).Msgf("Failed to read group %s", groupID) - errorcode.ItemNotFound.Render(w, r, http.StatusInternalServerError, "") + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing group id") return } - ctx := context.WithValue(r.Context(), groupIDKey, group) + 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) { - con, err := g.initLdap() + client, err := g.GetClient() if err != nil { - g.logger.Error().Err(err).Msg("Failed to initialize ldap") - errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, "") + g.logger.Error().Err(err).Msg("could not get client") + errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, err.Error()) return } - // TODO make filter configurable - result, err := g.ldapSearch(con, "(objectClass=posixGroup)", g.config.Ldap.BaseDNGroups) + search := r.URL.Query().Get("search") + if search == "" { + search = r.URL.Query().Get("$search") + } - if err != nil { - g.logger.Error().Err(err).Msg("Failed search ldap with filter: '(objectClass=posixGroup)'") - errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, "") + 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 + } + 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(result.Entries)) + groups := make([]*msgraph.Group, 0, len(res.Groups)) - for _, group := range result.Entries { - groups = append( - groups, - createGroupModelFromLDAP( - group, - ), - ) + for _, group := range res.Groups { + groups = append(groups, createGroupModelFromCS3(group)) } render.Status(r, http.StatusOK) @@ -72,8 +104,26 @@ func (g Graph) GetGroups(w http.ResponseWriter, r *http.Request) { // GetGroup implements the Service interface. func (g Graph) GetGroup(w http.ResponseWriter, r *http.Request) { - group := r.Context().Value(groupIDKey).(*ldap.Entry) + group := r.Context().Value(groupKey).(*cs3.Group) render.Status(r, http.StatusOK) - render.JSON(w, r, createGroupModelFromLDAP(group)) + 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? + } } diff --git a/graph/pkg/service/v0/ldap.go b/graph/pkg/service/v0/ldap.go deleted file mode 100644 index 10c8ec0cd..000000000 --- a/graph/pkg/service/v0/ldap.go +++ /dev/null @@ -1,94 +0,0 @@ -package svc - -import ( - "errors" - - "github.com/go-ldap/ldap/v3" - msgraph "github.com/yaegashi/msgraph.go/v1.0" -) - -func (g Graph) ldapGetSingleEntry(baseDn string, filter string) (*ldap.Entry, error) { - conn, err := g.initLdap() - if err != nil { - return nil, err - } - result, err := g.ldapSearch(conn, filter, baseDn) - if err != nil { - return nil, err - } - if len(result.Entries) == 0 { - return nil, errors.New("resource not found") - } - return result.Entries[0], nil -} - -func (g Graph) initLdap() (*ldap.Conn, error) { - g.logger.Info().Msgf("Dialing ldap %s://%s", g.config.Ldap.Network, g.config.Ldap.Address) - con, err := ldap.Dial(g.config.Ldap.Network, g.config.Ldap.Address) - - if err != nil { - return nil, err - } - - if err := con.Bind(g.config.Ldap.UserName, g.config.Ldap.Password); err != nil { - return nil, err - } - return con, nil -} - -func (g Graph) ldapSearch(con *ldap.Conn, filter string, baseDN string) (*ldap.SearchResult, error) { - search := ldap.NewSearchRequest( - baseDN, - ldap.ScopeWholeSubtree, - ldap.NeverDerefAliases, - 0, - 0, - false, - filter, - []string{"dn", - "uid", - "givenname", - "mail", - "displayname", - "entryuuid", - "sn", - "cn", - }, - nil, - ) - - return con.Search(search) -} - -func createUserModelFromLDAP(entry *ldap.Entry) *msgraph.User { - displayName := entry.GetAttributeValue("displayname") - givenName := entry.GetAttributeValue("givenname") - mail := entry.GetAttributeValue("mail") - surName := entry.GetAttributeValue("sn") - id := entry.GetAttributeValue("entryuuid") - return &msgraph.User{ - DisplayName: &displayName, - GivenName: &givenName, - Surname: &surName, - Mail: &mail, - DirectoryObject: msgraph.DirectoryObject{ - Entity: msgraph.Entity{ - ID: &id, - }, - }, - } -} - -func createGroupModelFromLDAP(entry *ldap.Entry) *msgraph.Group { - id := entry.GetAttributeValue("entryuuid") - displayName := entry.GetAttributeValue("cn") - - return &msgraph.Group{ - DisplayName: &displayName, - DirectoryObject: msgraph.DirectoryObject{ - Entity: msgraph.Entity{ - ID: &id, - }, - }, - } -} diff --git a/graph/pkg/service/v0/users.go b/graph/pkg/service/v0/users.go index a18eec6ea..4926f5a80 100644 --- a/graph/pkg/service/v0/users.go +++ b/graph/pkg/service/v0/users.go @@ -2,15 +2,16 @@ package svc import ( "context" - "fmt" "net/http" - userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + cs3 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" "github.com/cs3org/reva/pkg/user" "github.com/go-chi/chi" "github.com/go-chi/render" - "github.com/go-ldap/ldap/v3" "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" ) @@ -20,24 +21,41 @@ import ( // 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) { - var user *ldap.Entry - var err error userID := chi.URLParam(r, "userID") if userID == "" { - errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "") - return - } - // TODO make filter configurable - filter := fmt.Sprintf("(&(objectClass=posixAccount)(ownCloudUUID=%s))", userID) - user, err = g.ldapGetSingleEntry(g.config.Ldap.BaseDNUsers, filter) - if err != nil { - g.logger.Info().Err(err).Msgf("Failed to read user %s", userID) - errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, "") + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing user id") return } - ctx := context.WithValue(r.Context(), userIDKey, user) + 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)) }) } @@ -47,7 +65,8 @@ func (g Graph) GetMe(w http.ResponseWriter, r *http.Request) { u, ok := user.ContextGetUser(r.Context()) if !ok { - errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, "") + g.logger.Error().Msg("user not in context") + errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, "user not in context") return } @@ -59,11 +78,68 @@ func (g Graph) GetMe(w http.ResponseWriter, r *http.Request) { render.JSON(w, r, me) } -func createUserModelFromCS3(u *userpb.User) *msgraph.User { +// 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() + 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 + } + 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) + + 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 + // TODO u.Groups are those ids or group names? OnPremisesSamAccountName: &u.Username, DirectoryObject: msgraph.DirectoryObject{ Entity: msgraph.Entity{ @@ -78,45 +154,3 @@ func createUserModelFromCS3(u *userpb.User) *msgraph.User { }, } } - -// GetUsers implements the Service interface. -// TODO use cs3 api to look up user -func (g Graph) GetUsers(w http.ResponseWriter, r *http.Request) { - con, err := g.initLdap() - if err != nil { - g.logger.Error().Err(err).Msg("Failed to initialize ldap") - errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, "") - return - } - - // TODO make filter configurable - result, err := g.ldapSearch(con, "(objectClass=posixAccount)", g.config.Ldap.BaseDNUsers) - - if err != nil { - g.logger.Error().Err(err).Msg("Failed search ldap with filter: '(objectClass=posixAccount)'") - errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, "") - return - } - - users := make([]*msgraph.User, 0, len(result.Entries)) - - for _, user := range result.Entries { - users = append( - users, - createUserModelFromLDAP( - 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(userIDKey).(*ldap.Entry) - - render.Status(r, http.StatusOK) - render.JSON(w, r, createUserModelFromLDAP(user)) -} From 900f8db799e6c809a7478203eea46223a04675c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Friedrich=20Dreyer?= Date: Wed, 21 Jul 2021 09:22:39 +0000 Subject: [PATCH 3/3] add changelog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jörn Friedrich Dreyer --- changelog/unreleased/spaces-api.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelog/unreleased/spaces-api.md diff --git a/changelog/unreleased/spaces-api.md b/changelog/unreleased/spaces-api.md new file mode 100644 index 000000000..c332ccbdd --- /dev/null +++ b/changelog/unreleased/spaces-api.md @@ -0,0 +1,5 @@ +Enhancement: Refactor graph API + +We refactored the `/graph/v1.0/` endpoint which now relies on the internal acces token fer authentication, getting rid of any LDAP or OIDC code to authenticate requests. This allows using the graph api when using basic auth or any other auth mechanism provided by the CS3 auth providers / reva gateway / ocis proxy. + +https://github.com/owncloud/ocis/pull/2277