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 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 0e628bcd7..e678250f5 100644 --- a/graph/pkg/config/config.go +++ b/graph/pkg/config/config.go @@ -40,46 +40,31 @@ 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 } +// TokenManager is the config for using the reva token manager +type TokenManager struct { + JWTSecret string +} + type Spaces struct { WebDavBase string } // 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 - 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 f53b0788d..0481669ee 100644 --- a/graph/pkg/flagset/flagset.go +++ b/graph/pkg/flagset/flagset.go @@ -144,75 +144,18 @@ 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, + 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: "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: "reva-gateway-addr", Value: flags.OverrideDefaultString(cfg.Reva.Address, "127.0.0.1:9142"), @@ -220,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/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..0526ce0ef 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,7 @@ 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 +36,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..9026796a8 100644 --- a/graph/pkg/service/v0/drives.go +++ b/graph/pkg/service/v0/drives.go @@ -4,82 +4,59 @@ import ( "math" "net/http" "net/url" - "strings" + "path" "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" - + "github.com/go-chi/render" + "github.com/owncloud/ocis/graph/pkg/service/v0/errorcode" 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() 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 } - 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 { + 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 } @@ -90,52 +67,60 @@ 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 - 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 } - 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, - } - - 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 } @@ -150,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 6567d0212..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 @@ -17,6 +19,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 +51,7 @@ var errorCodes = [...]string{ "accessDenied", "activityLimitReached", "generalException", + "InvalidAuthenticationToken", "invalidRange", "invalidRequest", "itemNotFound", @@ -62,9 +67,20 @@ var errorCodes = [...]string{ } // Render writes an Graph ErrorObject to the response writer -func (e ErrorCode) Render(w http.ResponseWriter, r *http.Request, status int) { - resp := &msgraph.ErrorObject{ - Code: e.String(), +func (e ErrorCode) Render(w http.ResponseWriter, r *http.Request, status int, msg string) { + 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 c98619c21..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) - 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.StatusNotFound) + 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 ba20972b2..4926f5a80 100644 --- a/graph/pkg/service/v0/users.go +++ b/graph/pkg/service/v0/users.go @@ -2,92 +2,122 @@ package svc import ( "context" - "fmt" "net/http" - "github.com/owncloud/ocis/graph/pkg/service/v0/errorcode" - + 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/ocis-pkg/oidc" + "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) { - 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)) }) } // 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 { + g.logger.Error().Msg("user not in context") + errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, "user not in context") 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) } // 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() + + 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=posixAccount)", g.config.Ldap.BaseDNUsers) + 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=posixAccount)'") - errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError) + 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(result.Entries)) + users := make([]*msgraph.User, 0, len(res.Users)) - for _, user := range result.Entries { - users = append( - users, - createUserModelFromLDAP( - user, - ), - ) + for _, user := range res.Users { + users = append(users, createUserModelFromCS3(user)) } render.Status(r, http.StatusOK) @@ -96,8 +126,31 @@ func (g Graph) GetUsers(w http.ResponseWriter, r *http.Request) { // GetUser implements the Service interface. func (g Graph) GetUser(w http.ResponseWriter, r *http.Request) { - user := r.Context().Value(userIDKey).(*ldap.Entry) + user := r.Context().Value(userKey).(*cs3.User) render.Status(r, http.StatusOK) - render.JSON(w, r, createUserModelFromLDAP(user)) + 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, + }, + }, + }, + }, + } }