From 5070941dc4056cee3eb93713752b77d3684fc0ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Friedrich=20Dreyer?= Date: Fri, 9 Jul 2021 15:15:20 +0000 Subject: [PATCH] graph: refactor auth and middlewares MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jörn Friedrich Dreyer --- graph/pkg/config/config.go | 6 ++ graph/pkg/flagset/flagset.go | 7 ++ graph/pkg/middleware/auth.go | 78 +++++++++++++++++++++ graph/pkg/server/http/server.go | 16 ++--- graph/pkg/service/v0/drives.go | 39 ----------- graph/pkg/service/v0/errorcode/errorcode.go | 8 ++- graph/pkg/service/v0/groups.go | 8 +-- graph/pkg/service/v0/users.go | 51 +++++++++----- 8 files changed, 141 insertions(+), 72 deletions(-) create mode 100644 graph/pkg/middleware/auth.go diff --git a/graph/pkg/config/config.go b/graph/pkg/config/config.go index 0e628bcd7..ca1a5f934 100644 --- a/graph/pkg/config/config.go +++ b/graph/pkg/config/config.go @@ -63,6 +63,11 @@ type Reva struct { Address string } +// TokenManager is the config for using the reva token manager +type TokenManager struct { + JWTSecret string +} + type Spaces struct { WebDavBase string } @@ -79,6 +84,7 @@ type Config struct { Ldap Ldap OpenIDConnect OpenIDConnect Reva Reva + TokenManager TokenManager Spaces Spaces Context context.Context diff --git a/graph/pkg/flagset/flagset.go b/graph/pkg/flagset/flagset.go index f53b0788d..44028fd8b 100644 --- a/graph/pkg/flagset/flagset.go +++ b/graph/pkg/flagset/flagset.go @@ -213,6 +213,13 @@ func ServerWithConfig(cfg *config.Config) []cli.Flag { Destination: &cfg.OpenIDConnect.Realm, }, + &cli.StringFlag{ + Name: "jwt-secret", + Value: flags.OverrideDefaultString(cfg.TokenManager.JWTSecret, "Pive-Fumkiu4"), + Usage: "Used to validate the reva access JWT, should equal reva's jwt-secret", + EnvVars: []string{"GRAPH_JWT_SECRET", "OCIS_JWT_SECRET"}, + Destination: &cfg.TokenManager.JWTSecret, + }, &cli.StringFlag{ Name: "reva-gateway-addr", Value: flags.OverrideDefaultString(cfg.Reva.Address, "127.0.0.1:9142"), diff --git a/graph/pkg/middleware/auth.go b/graph/pkg/middleware/auth.go new file mode 100644 index 000000000..1436f16fd --- /dev/null +++ b/graph/pkg/middleware/auth.go @@ -0,0 +1,78 @@ +package middleware + +import ( + "net/http" + + "github.com/cs3org/reva/pkg/auth/scope" + "github.com/cs3org/reva/pkg/token" + "github.com/cs3org/reva/pkg/token/manager/jwt" + "github.com/cs3org/reva/pkg/user" + "github.com/owncloud/ocis/graph/pkg/service/v0/errorcode" + "github.com/owncloud/ocis/ocis-pkg/account" + "google.golang.org/grpc/metadata" +) + +// authOptions initializes the available default options. +func authOptions(opts ...account.Option) account.Options { + opt := account.Options{} + + for _, o := range opts { + o(&opt) + } + + return opt +} + +// Auth provides a middleware to authenticate requestrs using the x-access-token header value +// and write it to the context. If there is no x-access-token the middleware prevents access and renders a json document. +func Auth(opts ...account.Option) func(http.Handler) http.Handler { + opt := authOptions(opts...) + tokenManager, err := jwt.New(map[string]interface{}{ + "secret": opt.JWTSecret, + "expires": int64(60), + }) + if err != nil { + opt.Logger.Fatal().Err(err).Msgf("Could not initialize token-manager") + } + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + t := r.Header.Get("x-access-token") + if t == "" { + errorcode.InvalidAuthenticationToken.Render(w, r, http.StatusUnauthorized, "Access token is empty.") + /* msgraph error for GET https://graph.microsoft.com/v1.0/me + { + "error": + { + "code":"InvalidAuthenticationToken", + "message":"Access token is empty.", + "innerError":{ + "date":"2021-07-09T14:40:51", + "request-id":"bb12f7db-b4c4-43a9-ba4b-31676aeed019", + "client-request-id":"bb12f7db-b4c4-43a9-ba4b-31676aeed019" + } + } + } + */ + return + } + + u, tokenScope, err := tokenManager.DismantleToken(r.Context(), t) + if err != nil { + errorcode.InvalidAuthenticationToken.Render(w, r, http.StatusUnauthorized, "invalid token") + return + } + if ok, err := scope.VerifyScope(tokenScope, r); err != nil || !ok { + opt.Logger.Error().Err(err).Msg("verifying scope failed") + errorcode.InvalidAuthenticationToken.Render(w, r, http.StatusUnauthorized, "verifying scope failed") + return + } + + ctx = token.ContextSetToken(ctx, t) + ctx = user.ContextSetUser(ctx, u) + ctx = metadata.AppendToOutgoingContext(ctx, token.TokenHeader, t) + + next.ServeHTTP(w, r.WithContext(ctx)) + }) + } +} diff --git a/graph/pkg/server/http/server.go b/graph/pkg/server/http/server.go index 60f2498b9..c9d733e65 100644 --- a/graph/pkg/server/http/server.go +++ b/graph/pkg/server/http/server.go @@ -2,10 +2,11 @@ package http import ( "github.com/asim/go-micro/v3" + graphMiddleware "github.com/owncloud/ocis/graph/pkg/middleware" svc "github.com/owncloud/ocis/graph/pkg/service/v0" "github.com/owncloud/ocis/graph/pkg/version" + "github.com/owncloud/ocis/ocis-pkg/account" "github.com/owncloud/ocis/ocis-pkg/middleware" - "github.com/owncloud/ocis/ocis-pkg/oidc" "github.com/owncloud/ocis/ocis-pkg/service/http" ) @@ -27,11 +28,6 @@ func Server(opts ...Option) (http.Service, error) { svc.Logger(options.Logger), svc.Config(options.Config), svc.Middleware( - middleware.RealIP, - middleware.RequestID, - middleware.NoCache, - middleware.Cors, - middleware.Secure, middleware.Version( "graph", version.String, @@ -39,11 +35,9 @@ func Server(opts ...Option) (http.Service, error) { middleware.Logger( options.Logger, ), - middleware.OpenIDConnect( - oidc.Endpoint(options.Config.OpenIDConnect.Endpoint), - oidc.Realm(options.Config.OpenIDConnect.Realm), - oidc.Insecure(options.Config.OpenIDConnect.Insecure), - oidc.Logger(options.Logger), + graphMiddleware.Auth( + account.Logger(options.Logger), + account.JWTSecret(options.Config.TokenManager.JWTSecret), ), ), ) diff --git a/graph/pkg/service/v0/drives.go b/graph/pkg/service/v0/drives.go index df8454587..3677da347 100644 --- a/graph/pkg/service/v0/drives.go +++ b/graph/pkg/service/v0/drives.go @@ -8,42 +8,17 @@ import ( "time" "github.com/go-chi/render" - "google.golang.org/grpc/metadata" cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" - "github.com/cs3org/reva/pkg/token" msgraph "github.com/owncloud/open-graph-api-go" ) -func getToken(r *http.Request) string { - // 1. check Authorization header - hdr := r.Header.Get("Authorization") - t := strings.TrimPrefix(hdr, "Bearer ") - if t != "" { - return t - } - // TODO 2. check form encoded body parameter for POST requests, see https://tools.ietf.org/html/rfc6750#section-2.2 - - // 3. check uri query parameter, see https://tools.ietf.org/html/rfc6750#section-2.3 - tokens, ok := r.URL.Query()["access_token"] - if !ok || len(tokens[0]) < 1 { - return "" - } - - return tokens[0] -} - // GetDrives implements the Service interface. func (g Graph) GetDrives(w http.ResponseWriter, r *http.Request) { g.logger.Info().Msg("Calling GetDrives") - if getToken(r) == "" { - g.logger.Error().Msg("no access token provided in request") - w.WriteHeader(http.StatusForbidden) - return - } ctx := r.Context() client, err := g.GetClient() @@ -52,9 +27,6 @@ func (g Graph) GetDrives(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) return } - t := r.Header.Get("x-access-token") - ctx = token.ContextSetToken(ctx, t) - ctx = metadata.AppendToOutgoingContext(ctx, "x-access-token", t) res, err := client.ListStorageSpaces(ctx, &storageprovider.ListStorageSpacesRequest{}) if err != nil { @@ -90,11 +62,6 @@ func (g Graph) GetDrives(w http.ResponseWriter, r *http.Request) { // GetRootDriveChildren implements the Service interface. func (g Graph) GetRootDriveChildren(w http.ResponseWriter, r *http.Request) { g.logger.Info().Msg("Calling GetRootDriveChildren") - if getToken(r) == "" { - g.logger.Error().Msg("no access token provided in request") - w.WriteHeader(http.StatusForbidden) - return - } ctx := r.Context() fn := g.config.WebdavNamespace @@ -106,12 +73,6 @@ func (g Graph) GetRootDriveChildren(w http.ResponseWriter, r *http.Request) { return } - t := r.Header.Get("x-access-token") - ctx = token.ContextSetToken(ctx, t) - ctx = metadata.AppendToOutgoingContext(ctx, "x-access-token", t) - - g.logger.Info().Interface("context", ctx).Msg("provides access token") - ref := &storageprovider.Reference{ Path: fn, } diff --git a/graph/pkg/service/v0/errorcode/errorcode.go b/graph/pkg/service/v0/errorcode/errorcode.go index 6567d0212..c2d8805bf 100644 --- a/graph/pkg/service/v0/errorcode/errorcode.go +++ b/graph/pkg/service/v0/errorcode/errorcode.go @@ -17,6 +17,8 @@ const ( ActivityLimitReached // GeneralException defines the error if an unspecified error has occurred. GeneralException + // InvalidAuthenticationToken defines the error if the access token is missing + InvalidAuthenticationToken // InvalidRange defines the error if the specified byte range is invalid or unavailable. InvalidRange // InvalidRequest defines the error if the request is malformed or incorrect. @@ -47,6 +49,7 @@ var errorCodes = [...]string{ "accessDenied", "activityLimitReached", "generalException", + "InvalidAuthenticationToken", "invalidRange", "invalidRequest", "itemNotFound", @@ -62,9 +65,10 @@ var errorCodes = [...]string{ } // Render writes an Graph ErrorObject to the response writer -func (e ErrorCode) Render(w http.ResponseWriter, r *http.Request, status int) { +func (e ErrorCode) Render(w http.ResponseWriter, r *http.Request, status int, msg string) { resp := &msgraph.ErrorObject{ - Code: e.String(), + Code: e.String(), + Message: msg, } render.Status(r, status) render.JSON(w, r, resp) diff --git a/graph/pkg/service/v0/groups.go b/graph/pkg/service/v0/groups.go index c98619c21..2d0277da5 100644 --- a/graph/pkg/service/v0/groups.go +++ b/graph/pkg/service/v0/groups.go @@ -20,7 +20,7 @@ func (g Graph) GroupCtx(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { groupID := chi.URLParam(r, "groupID") if groupID == "" { - errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest) + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "groupID empty") return } // TODO make filter configurable @@ -28,7 +28,7 @@ func (g Graph) GroupCtx(next http.Handler) http.Handler { group, err := g.ldapGetSingleEntry(g.config.Ldap.BaseDNGroups, filter) if err != nil { g.logger.Info().Err(err).Msgf("Failed to read group %s", groupID) - errorcode.ItemNotFound.Render(w, r, http.StatusNotFound) + errorcode.ItemNotFound.Render(w, r, http.StatusInternalServerError, "") return } @@ -42,7 +42,7 @@ func (g Graph) GetGroups(w http.ResponseWriter, r *http.Request) { con, err := g.initLdap() if err != nil { g.logger.Error().Err(err).Msg("Failed to initialize ldap") - errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError) + errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, "") return } @@ -51,7 +51,7 @@ func (g Graph) GetGroups(w http.ResponseWriter, r *http.Request) { if err != nil { g.logger.Error().Err(err).Msg("Failed search ldap with filter: '(objectClass=posixGroup)'") - errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError) + errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, "") return } diff --git a/graph/pkg/service/v0/users.go b/graph/pkg/service/v0/users.go index ba20972b2..a18eec6ea 100644 --- a/graph/pkg/service/v0/users.go +++ b/graph/pkg/service/v0/users.go @@ -5,18 +5,19 @@ import ( "fmt" "net/http" - "github.com/owncloud/ocis/graph/pkg/service/v0/errorcode" - + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + "github.com/cs3org/reva/pkg/user" "github.com/go-chi/chi" "github.com/go-chi/render" "github.com/go-ldap/ldap/v3" - "github.com/owncloud/ocis/ocis-pkg/oidc" + "github.com/owncloud/ocis/graph/pkg/service/v0/errorcode" msgraph "github.com/yaegashi/msgraph.go/v1.0" ) // UserCtx middleware is used to load an User object from // the URL parameters passed through as the request. In case // the User could not be found, we stop here and return a 404. +// TODO use cs3 api to look up user func (g Graph) UserCtx(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var user *ldap.Entry @@ -24,7 +25,7 @@ func (g Graph) UserCtx(next http.Handler) http.Handler { userID := chi.URLParam(r, "userID") if userID == "" { - errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest) + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "") return } // TODO make filter configurable @@ -32,7 +33,7 @@ func (g Graph) UserCtx(next http.Handler) http.Handler { user, err = g.ldapGetSingleEntry(g.config.Ldap.BaseDNUsers, filter) if err != nil { g.logger.Info().Err(err).Msgf("Failed to read user %s", userID) - errorcode.ItemNotFound.Render(w, r, http.StatusNotFound) + errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, "") return } @@ -43,30 +44,48 @@ func (g Graph) UserCtx(next http.Handler) http.Handler { // GetMe implements the Service interface. func (g Graph) GetMe(w http.ResponseWriter, r *http.Request) { - claims := oidc.FromContext(r.Context()) - g.logger.Info().Interface("Claims", claims).Msg("Claims in /me") - // TODO make filter configurable - filter := fmt.Sprintf("(&(objectClass=posixAccount)(cn=%s))", claims.PreferredUsername) - user, err := g.ldapGetSingleEntry(g.config.Ldap.BaseDNUsers, filter) - if err != nil { - g.logger.Info().Err(err).Msgf("Failed to read user %s", claims.PreferredUsername) - errorcode.ItemNotFound.Render(w, r, http.StatusNotFound) + u, ok := user.ContextGetUser(r.Context()) + if !ok { + errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, "") return } - me := createUserModelFromLDAP(user) + g.logger.Info().Interface("user", u).Msg("User in /me") + + me := createUserModelFromCS3(u) render.Status(r, http.StatusOK) render.JSON(w, r, me) } +func createUserModelFromCS3(u *userpb.User) *msgraph.User { + return &msgraph.User{ + DisplayName: &u.DisplayName, + Mail: &u.Mail, + // TODO u.Groups + OnPremisesSamAccountName: &u.Username, + DirectoryObject: msgraph.DirectoryObject{ + Entity: msgraph.Entity{ + ID: &u.Id.OpaqueId, + Object: msgraph.Object{ + AdditionalData: map[string]interface{}{ + "uidnumber": u.UidNumber, + "gidnumber": u.GidNumber, + }, + }, + }, + }, + } +} + // GetUsers implements the Service interface. +// TODO use cs3 api to look up user func (g Graph) GetUsers(w http.ResponseWriter, r *http.Request) { con, err := g.initLdap() if err != nil { g.logger.Error().Err(err).Msg("Failed to initialize ldap") - errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError) + errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, "") return } @@ -75,7 +94,7 @@ func (g Graph) GetUsers(w http.ResponseWriter, r *http.Request) { if err != nil { g.logger.Error().Err(err).Msg("Failed search ldap with filter: '(objectClass=posixAccount)'") - errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError) + errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, "") return }