Merge pull request #2277 from owncloud/spaces-api

graph refactorings
This commit is contained in:
Jörn Friedrich Dreyer
2021-07-21 12:59:11 +02:00
committed by GitHub
13 changed files with 373 additions and 366 deletions
+5
View File
@@ -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
+1 -1
View File
@@ -6,12 +6,12 @@ require (
contrib.go.opencensus.io/exporter/jaeger v0.2.1 contrib.go.opencensus.io/exporter/jaeger v0.2.1
contrib.go.opencensus.io/exporter/ocagent v0.7.0 contrib.go.opencensus.io/exporter/ocagent v0.7.0
contrib.go.opencensus.io/exporter/zipkin v0.1.2 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/asim/go-micro/v3 v3.5.1-0.20210217182006-0f0ace1a44a9
github.com/cs3org/go-cs3apis v0.0.0-20210702091910-85a56bfd027f github.com/cs3org/go-cs3apis v0.0.0-20210702091910-85a56bfd027f
github.com/cs3org/reva v1.10.0 github.com/cs3org/reva v1.10.0
github.com/go-chi/chi v4.1.2+incompatible github.com/go-chi/chi v4.1.2+incompatible
github.com/go-chi/render v1.0.1 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/micro/cli/v2 v2.1.2
github.com/oklog/run v1.1.0 github.com/oklog/run v1.1.0
github.com/openzipkin/zipkin-go v0.2.5 github.com/openzipkin/zipkin-go v0.2.5
-4
View File
@@ -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 h1:8sne7z4pe+9rkGP3ZX2i3Sx1FMQ8hhBs5QCb4VVvZJI=
github.com/cs3org/reva v1.10.0/go.mod h1:4bpcovnx3EAetafPIp4Fia1GkFvjFDkztacmCWI7cN0= 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/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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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.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 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/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 v0.0.0-20180614180643-0dae4fefe7c0/go.mod h1:IiEW3SEiiErVyFdH8NTuWjSifiEQKUoyK3LNqr2kCHU=
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= 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= 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/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= 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/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/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 h1:GX3X+1Qaewtj9flL2wgoTBfLA5NcmrCY39TJRpPbUrI=
github.com/thejerf/suture/v4 v4.0.0/go.mod h1:g0e8vwskm9tI0jRjxrnA6lSr0q6OfPdWJVX7G5bVWRs= 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 h1:Klz8I9kdtkIN6EpHHUOMLCYhTn/2WAe5a0s1hcBkdTI=
google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= 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/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-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-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+14 -29
View File
@@ -40,46 +40,31 @@ type Tracing struct {
Service string 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. // Reva defines all available REVA configuration.
type Reva struct { type Reva struct {
Address string Address string
} }
// TokenManager is the config for using the reva token manager
type TokenManager struct {
JWTSecret string
}
type Spaces struct { type Spaces struct {
WebDavBase string WebDavBase string
} }
// Config combines all available configuration parts. // Config combines all available configuration parts.
type Config struct { type Config struct {
File string File string
WebdavNamespace string Log Log
Log Log Debug Debug
Debug Debug HTTP HTTP
HTTP HTTP Server Server
Server Server Tracing Tracing
Tracing Tracing Reva Reva
Ldap Ldap TokenManager TokenManager
OpenIDConnect OpenIDConnect Spaces Spaces
Reva Reva
Spaces Spaces
Context context.Context Context context.Context
Supervised bool Supervised bool
+6 -70
View File
@@ -144,75 +144,18 @@ func ServerWithConfig(cfg *config.Config) []cli.Flag {
&cli.StringFlag{ &cli.StringFlag{
Name: "spaces-webdav-base", Name: "spaces-webdav-base",
Value: flags.OverrideDefaultString(cfg.Spaces.WebDavBase, "https://localhost:9200/dav/spaces/"), 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"}, EnvVars: []string{"GRAPH_SPACES_WEBDAV_BASE"},
Destination: &cfg.Spaces.WebDavBase, Destination: &cfg.Spaces.WebDavBase,
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: "ldap-network", Name: "jwt-secret",
Value: flags.OverrideDefaultString(cfg.Ldap.Network, "tcp"), Value: flags.OverrideDefaultString(cfg.TokenManager.JWTSecret, "Pive-Fumkiu4"),
Usage: "Network protocol to use to connect to the Ldap server", Usage: "Used to validate the reva access JWT, should equal reva's jwt-secret",
EnvVars: []string{"GRAPH_LDAP_NETWORK"}, EnvVars: []string{"GRAPH_JWT_SECRET", "OCIS_JWT_SECRET"},
Destination: &cfg.Ldap.Network, 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{ &cli.StringFlag{
Name: "reva-gateway-addr", Name: "reva-gateway-addr",
Value: flags.OverrideDefaultString(cfg.Reva.Address, "127.0.0.1:9142"), 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"}, EnvVars: []string{"REVA_GATEWAY_ADDR"},
Destination: &cfg.Reva.Address, 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{ &cli.StringFlag{
Name: "extensions", Name: "extensions",
Usage: "Run specific extensions during supervised mode. This flag is set by the runtime", Usage: "Run specific extensions during supervised mode. This flag is set by the runtime",
+78
View File
@@ -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))
})
}
}
+5 -10
View File
@@ -2,10 +2,11 @@ package http
import ( import (
"github.com/asim/go-micro/v3" "github.com/asim/go-micro/v3"
graphMiddleware "github.com/owncloud/ocis/graph/pkg/middleware"
svc "github.com/owncloud/ocis/graph/pkg/service/v0" svc "github.com/owncloud/ocis/graph/pkg/service/v0"
"github.com/owncloud/ocis/graph/pkg/version" "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/middleware"
"github.com/owncloud/ocis/ocis-pkg/oidc"
"github.com/owncloud/ocis/ocis-pkg/service/http" "github.com/owncloud/ocis/ocis-pkg/service/http"
) )
@@ -27,11 +28,7 @@ func Server(opts ...Option) (http.Service, error) {
svc.Logger(options.Logger), svc.Logger(options.Logger),
svc.Config(options.Config), svc.Config(options.Config),
svc.Middleware( svc.Middleware(
middleware.RealIP,
middleware.RequestID, middleware.RequestID,
middleware.NoCache,
middleware.Cors,
middleware.Secure,
middleware.Version( middleware.Version(
"graph", "graph",
version.String, version.String,
@@ -39,11 +36,9 @@ func Server(opts ...Option) (http.Service, error) {
middleware.Logger( middleware.Logger(
options.Logger, options.Logger,
), ),
middleware.OpenIDConnect( graphMiddleware.Auth(
oidc.Endpoint(options.Config.OpenIDConnect.Endpoint), account.Logger(options.Logger),
oidc.Realm(options.Config.OpenIDConnect.Realm), account.JWTSecret(options.Config.TokenManager.JWTSecret),
oidc.Insecure(options.Config.OpenIDConnect.Insecure),
oidc.Logger(options.Logger),
), ),
), ),
) )
+63 -78
View File
@@ -4,82 +4,59 @@ import (
"math" "math"
"net/http" "net/http"
"net/url" "net/url"
"strings" "path"
"time" "time"
"github.com/go-chi/render"
"google.golang.org/grpc/metadata"
cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/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" 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. // GetDrives implements the Service interface.
func (g Graph) GetDrives(w http.ResponseWriter, r *http.Request) { func (g Graph) GetDrives(w http.ResponseWriter, r *http.Request) {
g.logger.Info().Msg("Calling GetDrives") 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() ctx := r.Context()
client, err := g.GetClient() client, err := g.GetClient()
if err != nil { if err != nil {
g.logger.Err(err).Msg("error getting grpc client") g.logger.Err(err).Msg("error getting grpc client")
w.WriteHeader(http.StatusInternalServerError) errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return 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{}) res, err := client.ListStorageSpaces(ctx, &storageprovider.ListStorageSpacesRequest{
if err != nil { // TODO add filters?
})
switch {
case err != nil:
g.logger.Error().Err(err).Msg("error sending list storage spaces grpc request") g.logger.Error().Err(err).Msg("error sending list storage spaces grpc request")
w.WriteHeader(http.StatusInternalServerError) 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).Interface("status", res.Status).Msg("error calling grpc list storage spaces")
w.WriteHeader(http.StatusInternalServerError)
return 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) wdu, err := url.Parse(g.config.Spaces.WebDavBase)
if err != nil { if err != nil {
g.logger.Error().Err(err).Msg("error parsing url") g.logger.Error().Err(err).Msg("error parsing url")
w.WriteHeader(http.StatusInternalServerError) errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return return
} }
files, err := formatDrives(wdu, res.StorageSpaces) files, err := formatDrives(wdu, res.StorageSpaces)
if err != nil { if err != nil {
g.logger.Error().Err(err).Msg("error encoding response as json") 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 return
} }
@@ -90,52 +67,60 @@ func (g Graph) GetDrives(w http.ResponseWriter, r *http.Request) {
// GetRootDriveChildren implements the Service interface. // GetRootDriveChildren implements the Service interface.
func (g Graph) GetRootDriveChildren(w http.ResponseWriter, r *http.Request) { func (g Graph) GetRootDriveChildren(w http.ResponseWriter, r *http.Request) {
g.logger.Info().Msg("Calling GetRootDriveChildren") 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() ctx := r.Context()
fn := g.config.WebdavNamespace
client, err := g.GetClient() client, err := g.GetClient()
if err != nil { if err != nil {
g.logger.Err(err).Msg("error getting grpc client") g.logger.Error().Err(err).Msg("could not get client")
w.WriteHeader(http.StatusInternalServerError) errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, err.Error())
return return
} }
t := r.Header.Get("x-access-token") res, err := client.GetHome(ctx, &storageprovider.GetHomeRequest{})
ctx = token.ContextSetToken(ctx, t) switch {
ctx = metadata.AppendToOutgoingContext(ctx, "x-access-token", t) case err != nil:
g.logger.Error().Err(err).Msg("error sending get home grpc request")
g.logger.Info().Interface("context", ctx).Msg("provides access token") errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, err.Error())
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)
return return
} case res.Status.Code != cs3rpc.Code_CODE_OK:
// TODO handle not found and other status codes if res.Status.Code == cs3rpc.Code_CODE_NOT_FOUND {
if res.Status.Code != cs3rpc.Code_CODE_OK { errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, res.Status.Message)
g.logger.Error().Err(err).Str("path", fn).Interface("status", res.Status).Msg("error calling grpc list container") return
w.WriteHeader(http.StatusInternalServerError) }
g.logger.Error().Err(err).Msg("error sending get home grpc request")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, res.Status.Message)
return 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 { if err != nil {
g.logger.Error().Err(err).Msg("error encoding response as json") 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 return
} }
@@ -150,7 +135,7 @@ func cs3TimestampToTime(t *types.Timestamp) time.Time {
func cs3ResourceToDriveItem(res *storageprovider.ResourceInfo) (*msgraph.DriveItem, error) { func cs3ResourceToDriveItem(res *storageprovider.ResourceInfo) (*msgraph.DriveItem, error) {
size := new(int64) size := new(int64)
*size = int64(res.Size) // uint64 -> int :boom: *size = int64(res.Size) // uint64 -> int :boom:
name := strings.TrimPrefix(res.Path, "/home/") name := path.Base(res.Path)
driveItem := &msgraph.DriveItem{ driveItem := &msgraph.DriveItem{
BaseItem: msgraph.BaseItem{ BaseItem: msgraph.BaseItem{
+20 -4
View File
@@ -2,9 +2,11 @@ package errorcode
import ( import (
"net/http" "net/http"
"time"
"github.com/go-chi/render" "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 // 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 ActivityLimitReached
// GeneralException defines the error if an unspecified error has occurred. // GeneralException defines the error if an unspecified error has occurred.
GeneralException 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 defines the error if the specified byte range is invalid or unavailable.
InvalidRange InvalidRange
// InvalidRequest defines the error if the request is malformed or incorrect. // InvalidRequest defines the error if the request is malformed or incorrect.
@@ -47,6 +51,7 @@ var errorCodes = [...]string{
"accessDenied", "accessDenied",
"activityLimitReached", "activityLimitReached",
"generalException", "generalException",
"InvalidAuthenticationToken",
"invalidRange", "invalidRange",
"invalidRequest", "invalidRequest",
"itemNotFound", "itemNotFound",
@@ -62,9 +67,20 @@ var errorCodes = [...]string{
} }
// Render writes an Graph ErrorObject to the response writer // 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{ innererror := map[string]interface{}{
Code: e.String(), "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.Status(r, status)
render.JSON(w, r, resp) render.JSON(w, r, resp)
+4 -2
View File
@@ -31,8 +31,10 @@ func (g Graph) GetClient() (gateway.GatewayAPIClient, error) {
// other packages. // other packages.
type key int type key int
const userIDKey key = 0 const (
const groupIDKey key = 1 userKey key = iota
groupKey
)
type listResponse struct { type listResponse struct {
Value interface{} `json:"value,omitempty"` Value interface{} `json:"value,omitempty"`
+80 -30
View File
@@ -2,14 +2,16 @@ package svc
import ( import (
"context" "context"
"fmt"
"net/http" "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/owncloud/ocis/graph/pkg/service/v0/errorcode"
"github.com/go-chi/chi" "github.com/go-chi/chi"
"github.com/go-chi/render" "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" 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) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
groupID := chi.URLParam(r, "groupID") groupID := chi.URLParam(r, "groupID")
if groupID == "" { if groupID == "" {
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest) errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing group id")
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)
return 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)) next.ServeHTTP(w, r.WithContext(ctx))
}) })
} }
// GetGroups implements the Service interface. // GetGroups implements the Service interface.
func (g Graph) GetGroups(w http.ResponseWriter, r *http.Request) { func (g Graph) GetGroups(w http.ResponseWriter, r *http.Request) {
con, err := g.initLdap() client, err := g.GetClient()
if err != nil { if err != nil {
g.logger.Error().Err(err).Msg("Failed to initialize ldap") g.logger.Error().Err(err).Msg("could not get client")
errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError) errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, err.Error())
return return
} }
// TODO make filter configurable search := r.URL.Query().Get("search")
result, err := g.ldapSearch(con, "(objectClass=posixGroup)", g.config.Ldap.BaseDNGroups) if search == "" {
search = r.URL.Query().Get("$search")
}
if err != nil { res, err := client.FindGroups(r.Context(), &cs3.FindGroupsRequest{
g.logger.Error().Err(err).Msg("Failed search ldap with filter: '(objectClass=posixGroup)'") // FIXME presence match is currently not implemented, an empty search currently leads to
errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError) // 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 return
} }
groups := make([]*msgraph.Group, 0, len(result.Entries)) groups := make([]*msgraph.Group, 0, len(res.Groups))
for _, group := range result.Entries { for _, group := range res.Groups {
groups = append( groups = append(groups, createGroupModelFromCS3(group))
groups,
createGroupModelFromLDAP(
group,
),
)
} }
render.Status(r, http.StatusOK) render.Status(r, http.StatusOK)
@@ -72,8 +104,26 @@ func (g Graph) GetGroups(w http.ResponseWriter, r *http.Request) {
// GetGroup implements the Service interface. // GetGroup implements the Service interface.
func (g Graph) GetGroup(w http.ResponseWriter, r *http.Request) { 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.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?
}
} }
-94
View File
@@ -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,
},
},
}
}
+97 -44
View File
@@ -2,92 +2,122 @@ package svc
import ( import (
"context" "context"
"fmt"
"net/http" "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/chi"
"github.com/go-chi/render" "github.com/go-chi/render"
"github.com/go-ldap/ldap/v3" "github.com/owncloud/ocis/graph/pkg/service/v0/errorcode"
"github.com/owncloud/ocis/ocis-pkg/oidc"
//msgraph "github.com/owncloud/open-graph-api-go" // FIXME needs OnPremisesSamAccountName, OnPremisesDomainName and AdditionalData
msgraph "github.com/yaegashi/msgraph.go/v1.0" msgraph "github.com/yaegashi/msgraph.go/v1.0"
) )
// UserCtx middleware is used to load an User object from // UserCtx middleware is used to load an User object from
// the URL parameters passed through as the request. In case // the URL parameters passed through as the request. In case
// the User could not be found, we stop here and return a 404. // 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 { func (g Graph) UserCtx(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var user *ldap.Entry
var err error
userID := chi.URLParam(r, "userID") userID := chi.URLParam(r, "userID")
if userID == "" { if userID == "" {
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest) errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing user id")
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)
return 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)) next.ServeHTTP(w, r.WithContext(ctx))
}) })
} }
// GetMe implements the Service interface. // GetMe implements the Service interface.
func (g Graph) GetMe(w http.ResponseWriter, r *http.Request) { 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 u, ok := user.ContextGetUser(r.Context())
filter := fmt.Sprintf("(&(objectClass=posixAccount)(cn=%s))", claims.PreferredUsername) if !ok {
user, err := g.ldapGetSingleEntry(g.config.Ldap.BaseDNUsers, filter) g.logger.Error().Msg("user not in context")
if err != nil { errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, "user not in context")
g.logger.Info().Err(err).Msgf("Failed to read user %s", claims.PreferredUsername)
errorcode.ItemNotFound.Render(w, r, http.StatusNotFound)
return return
} }
me := createUserModelFromLDAP(user) g.logger.Info().Interface("user", u).Msg("User in /me")
me := createUserModelFromCS3(u)
render.Status(r, http.StatusOK) render.Status(r, http.StatusOK)
render.JSON(w, r, me) render.JSON(w, r, me)
} }
// GetUsers implements the Service interface. // GetUsers implements the Service interface.
// TODO use cs3 api to look up user
func (g Graph) GetUsers(w http.ResponseWriter, r *http.Request) { func (g Graph) GetUsers(w http.ResponseWriter, r *http.Request) {
con, err := g.initLdap()
client, err := g.GetClient()
if err != nil { if err != nil {
g.logger.Error().Err(err).Msg("Failed to initialize ldap") g.logger.Error().Err(err).Msg("could not get client")
errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError) errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, err.Error())
return return
} }
// TODO make filter configurable search := r.URL.Query().Get("search")
result, err := g.ldapSearch(con, "(objectClass=posixAccount)", g.config.Ldap.BaseDNUsers) if search == "" {
search = r.URL.Query().Get("$search")
}
if err != nil { res, err := client.FindUsers(r.Context(), &cs3.FindUsersRequest{
g.logger.Error().Err(err).Msg("Failed search ldap with filter: '(objectClass=posixAccount)'") // FIXME presence match is currently not implemented, an empty search currently leads to
errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError) // 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 return
} }
users := make([]*msgraph.User, 0, len(result.Entries)) users := make([]*msgraph.User, 0, len(res.Users))
for _, user := range result.Entries { for _, user := range res.Users {
users = append( users = append(users, createUserModelFromCS3(user))
users,
createUserModelFromLDAP(
user,
),
)
} }
render.Status(r, http.StatusOK) render.Status(r, http.StatusOK)
@@ -96,8 +126,31 @@ func (g Graph) GetUsers(w http.ResponseWriter, r *http.Request) {
// GetUser implements the Service interface. // GetUser implements the Service interface.
func (g Graph) GetUser(w http.ResponseWriter, r *http.Request) { 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.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,
},
},
},
},
}
} }