graph: refactor auth and middlewares

Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
This commit is contained in:
Jörn Friedrich Dreyer
2021-07-09 15:15:20 +00:00
parent 1af75187c1
commit 5070941dc4
8 changed files with 141 additions and 72 deletions

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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