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

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

View File

@@ -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

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/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=

View File

@@ -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

View File

@@ -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",

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))
})
}
}

View File

@@ -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),
),
),
)

View File

@@ -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{

View File

@@ -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)

View File

@@ -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"`

View File

@@ -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?
}
}

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,
},
},
}
}

View File

@@ -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,
},
},
},
},
}
}