From 34ff1f6e32a6149808ea37fd89f8ba95b5ba7cdd Mon Sep 17 00:00:00 2001 From: Abin Simon Date: Mon, 9 May 2022 10:13:05 +0530 Subject: [PATCH 1/2] Add auth service --- main.go | 10 ++++ pkg/auth/v3/interceptor.go | 2 +- pkg/auth/v3/middleware.go | 113 ++++++++++++++++++++++++++++++++++++- pkg/auth/v3/service.go | 24 ++++++-- server/auth.go | 22 ++++++++ 5 files changed, 163 insertions(+), 8 deletions(-) create mode 100644 server/auth.go diff --git a/main.go b/main.go index 9b6c56d..150f8d5 100644 --- a/main.go +++ b/main.go @@ -31,6 +31,7 @@ import ( sentryrpc "github.com/RafayLabs/rcloud-base/proto/rpc/sentry" systemrpc "github.com/RafayLabs/rcloud-base/proto/rpc/system" userrpc "github.com/RafayLabs/rcloud-base/proto/rpc/user" + authrpc "github.com/RafayLabs/rcloud-base/proto/rpc/v3" "github.com/RafayLabs/rcloud-base/server" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" kclient "github.com/ory/kratos-client-go" @@ -577,14 +578,17 @@ func runRPC(wg *sync.WaitGroup, ctx context.Context) { } var opts []_grpc.ServerOption + var asv authv3.AuthService if !dev { _log.Infow("adding auth interceptor") ac := authv3.NewAuthContext(kc, ks, as) + asv = authv3.NewAuthService(ac) o := authv3.Option{ ExcludeRPCMethods: []string{ "/rafay.dev.sentry.rpc.Bootstrap/GetBootstrapAgentTemplate", "/rafay.dev.sentry.rpc.Bootstrap/RegisterBootstrapAgent", "/rafay.dev.sentry.rpc.KubeConfig/GetForClusterWebSession", //TODO: enable auth from prompt + "/rafay.dev.rpc.v3.Auth/IsRequestAllowed", }, ExcludeAuthzMethods: []string{ "/rafay.dev.rpc.v3.User/GetUserInfo", @@ -625,6 +629,12 @@ func runRPC(wg *sync.WaitGroup, ctx context.Context) { auditrpc.RegisterAuditLogServer(s, auditLogServer) auditrpc.RegisterRelayAuditServer(s, relayAuditServer) + if !dev { + // if !dev, we should have the asv populated + authServer := server.NewAuthServer(asv) + authrpc.RegisterAuthServer(s, authServer) + } + _log.Infow("starting rpc server", "port", rpcPort) err = s.Serve(l) if err != nil { diff --git a/pkg/auth/v3/interceptor.go b/pkg/auth/v3/interceptor.go index 6eceb99..35ba58f 100644 --- a/pkg/auth/v3/interceptor.go +++ b/pkg/auth/v3/interceptor.go @@ -110,7 +110,7 @@ func (ac authContext) NewAuthUnaryInterceptor(opt Option) grpc.UnaryServerInterc NoAuthz: noAuthz, // FIXME: any better way to do this? } - res, err := ac.IsRequestAllowed(ctx, nil, acReq) + res, err := ac.IsRequestAllowed(ctx, acReq) if err != nil { _log.Errorf("Failed to authenticate a request: %s", err) return nil, status.Error(codes.Internal, codes.Internal.String()) diff --git a/pkg/auth/v3/middleware.go b/pkg/auth/v3/middleware.go index c2aca8a..1dd16f6 100644 --- a/pkg/auth/v3/middleware.go +++ b/pkg/auth/v3/middleware.go @@ -9,12 +9,14 @@ import ( "github.com/RafayLabs/rcloud-base/internal/dao" "github.com/RafayLabs/rcloud-base/pkg/common" + rpcv3 "github.com/RafayLabs/rcloud-base/proto/rpc/v3" commonpbv3 "github.com/RafayLabs/rcloud-base/proto/types/commonpb/v3" "github.com/uptrace/bun" "github.com/uptrace/bun/dialect/pgdialect" "github.com/uptrace/bun/driver/pgdriver" "github.com/urfave/negroni" "go.uber.org/zap" + grpc "google.golang.org/grpc" ) type authMiddleware struct { @@ -23,6 +25,12 @@ type authMiddleware struct { opt Option } +type remoteAuthMiddleware struct { + as rpcv3.AuthClient + db *bun.DB + opt Option +} + func NewAuthMiddleware(al *zap.Logger, opt Option) negroni.Handler { // Initialize database sqldb := sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(getDSN()))) @@ -33,6 +41,21 @@ func NewAuthMiddleware(al *zap.Logger, opt Option) negroni.Handler { } } +func NewRemoteAuthMiddleware(al *zap.Logger, as string, opt Option) negroni.Handler { + sqldb := sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(getDSN()))) + conn, err := grpc.Dial(as, grpc.WithInsecure()) + if err != nil { + _log.Fatal("Unable to connect to server", err) + } + client := rpcv3.NewAuthClient(conn) + + return &remoteAuthMiddleware{ + as: client, + opt: opt, + db: bun.NewDB(sqldb, pgdialect.New()), + } +} + func (am *authMiddleware) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { for _, ex := range am.opt.ExcludeURLs { match, err := regexp.MatchString(ex, r.URL.Path) @@ -83,7 +106,7 @@ func (am *authMiddleware) ServeHTTP(rw http.ResponseWriter, r *http.Request, nex Project: poResp.Project, Org: poResp.Organization, } - res, err := am.ac.IsRequestAllowed(r.Context(), r, req) + res, err := am.ac.IsRequestAllowed(r.Context(), req) if err != nil { _log.Errorf("Failed to authenticate a request: %s", err) http.Error(rw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -93,7 +116,93 @@ func (am *authMiddleware) ServeHTTP(rw http.ResponseWriter, r *http.Request, nex s := res.GetStatus() switch s { case commonpbv3.RequestStatus_RequestAllowed: - //udpate the session data response to be used within prompt + // update the session data response to be used within prompt + res.SessionData.Organization = poResp.OrganizationId + res.SessionData.Partner = poResp.PartnerId + res.SessionData.Project = &commonpbv3.ProjectData{ + List: []*commonpbv3.ProjectRole{ + { + ProjectId: poResp.ProjectId, + }, + }, + } + ctx := context.WithValue(r.Context(), common.SessionDataKey, res.SessionData) + next(rw, r.WithContext(ctx)) + return + case commonpbv3.RequestStatus_RequestMethodOrURLNotAllowed: + http.Error(rw, res.GetReason(), http.StatusForbidden) + return + case commonpbv3.RequestStatus_RequestNotAuthenticated: + http.Error(rw, res.GetReason(), http.StatusUnauthorized) + return + } + + // status is unknown + http.Error(rw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) +} + +// Same as previous ServeHTTP, just using remoteAuthMiddleware instead of authMiddleware +func (am *remoteAuthMiddleware) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + for _, ex := range am.opt.ExcludeURLs { + match, err := regexp.MatchString(ex, r.URL.Path) + if err != nil { + _log.Errorf("failed to match URL expression", err) + http.Error(rw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + if match { + next(rw, r) + return + } + } + // Auth is primarily done via grpc endpoints, this is only used + // for endoints which do not go through grpc As of now, it is just + // prompt. + var poResp dao.ProjectOrg + + if strings.HasPrefix(r.URL.String(), "/v2/debug/prompt/project/") { + // /v2/debug/prompt/project/:project/cluster/:cluster_name + splits := strings.Split(r.URL.String(), "/") + if len(splits) > 5 { + // we have to fetch the org info for casbin + res, err := dao.GetProjectOrganization(r.Context(), am.db, splits[5]) + if err != nil { + _log.Errorf("Failed to authenticate: unable to find project") + http.Error(rw, http.StatusText(http.StatusForbidden), http.StatusForbidden) + return + } + _log.Info("found project with organization ", res.Organization) + poResp = res + } + } else { + // The middleware to only used with routes which does not have + // a grpc and so fail for any other requests. + _log.Errorf("Failed to authenticate: not a prompt request") + http.Error(rw, http.StatusText(http.StatusForbidden), http.StatusForbidden) + return + } + + req := &commonpbv3.IsRequestAllowedRequest{ + Url: r.URL.String(), + Method: r.Method, + XSessionToken: r.Header.Get("X-Session-Token"), + XApiKey: r.Header.Get("X-API-KEYID"), + XApiToken: r.Header.Get("X-API-TOKEN"), + Cookie: r.Header.Get("Cookie"), + Project: poResp.Project, + Org: poResp.Organization, + } + res, err := am.as.IsRequestAllowed(r.Context(), req) + if err != nil { + _log.Errorf("Failed to authenticate a request: %s", err) + http.Error(rw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + s := res.GetStatus() + switch s { + case commonpbv3.RequestStatus_RequestAllowed: + // update the session data response to be used within prompt res.SessionData.Organization = poResp.OrganizationId res.SessionData.Partner = poResp.PartnerId res.SessionData.Project = &commonpbv3.ProjectData{ diff --git a/pkg/auth/v3/service.go b/pkg/auth/v3/service.go index 3ca6ac6..28339f8 100644 --- a/pkg/auth/v3/service.go +++ b/pkg/auth/v3/service.go @@ -5,7 +5,6 @@ import ( "crypto/md5" "encoding/base64" "errors" - "net/http" "strings" rpcv3 "github.com/RafayLabs/rcloud-base/proto/rpc/user" @@ -20,14 +19,30 @@ var ( ErrInvalidSignature = errors.New("invalid signature") ) -func (ac *authContext) IsRequestAllowed(ctx context.Context, httpreq *http.Request, req *commonv3.IsRequestAllowedRequest) (*commonv3.IsRequestAllowedResponse, error) { +type authService struct { + ac authContext +} + +type AuthService interface { + IsRequestAllowed(context.Context, *commonv3.IsRequestAllowedRequest) (*commonv3.IsRequestAllowedResponse, error) +} + +func NewAuthService(ac authContext) AuthService { + return &authService{ac} +} + +func (s *authService) IsRequestAllowed(ctx context.Context, req *commonv3.IsRequestAllowedRequest) (*commonv3.IsRequestAllowedResponse, error) { + return s.ac.IsRequestAllowed(ctx, req) +} + +func (ac *authContext) IsRequestAllowed(ctx context.Context, req *commonv3.IsRequestAllowedRequest) (*commonv3.IsRequestAllowedResponse, error) { res := &commonv3.IsRequestAllowedResponse{ Status: commonv3.RequestStatus_Unknown, SessionData: &commonv3.SessionData{}, } // Authenticate request - succ, err := ac.authenticate(ctx, httpreq, req, res) + succ, err := ac.authenticate(ctx, req, res) if err != nil { return nil, err } @@ -57,7 +72,7 @@ func getTokenCheckSum(body []byte) string { // authenticate validate whether the request is from a legitimate user // and populate relevant information in res. -func (ac *authContext) authenticate(ctx context.Context, httpreq *http.Request, req *commonv3.IsRequestAllowedRequest, res *commonv3.IsRequestAllowedResponse) (bool, error) { +func (ac *authContext) authenticate(ctx context.Context, req *commonv3.IsRequestAllowedRequest, res *commonv3.IsRequestAllowedResponse) (bool, error) { if len(req.XApiKey) > 0 && len(req.XSessionToken) == 0 { resp, err := ac.ks.GetByKey(ctx, &rpcv3.ApiKeyRequest{ Id: req.XApiKey, @@ -92,7 +107,6 @@ func (ac *authContext) authenticate(ctx context.Context, httpreq *http.Request, res.Status = commonv3.RequestStatus_RequestAllowed res.SessionData.Account = session.Identity.GetId() - // TODO: Better way to access traits t := session.Identity.Traits.(map[string]interface{}) res.SessionData.Username = t["email"].(string) } else { diff --git a/server/auth.go b/server/auth.go new file mode 100644 index 0000000..5da4267 --- /dev/null +++ b/server/auth.go @@ -0,0 +1,22 @@ +package server + +import ( + "context" + + authv3 "github.com/RafayLabs/rcloud-base/pkg/auth/v3" + rpcv3 "github.com/RafayLabs/rcloud-base/proto/rpc/v3" + v3 "github.com/RafayLabs/rcloud-base/proto/types/commonpb/v3" +) + +type authServer struct { + as authv3.AuthService +} + +// NewAuthServer returns new auth server implementation +func NewAuthServer(as authv3.AuthService) rpcv3.AuthServer { + return &authServer{as} +} + +func (s *authServer) IsRequestAllowed(ctx context.Context, ira *v3.IsRequestAllowedRequest) (*v3.IsRequestAllowedResponse, error) { + return s.as.IsRequestAllowed(ctx, ira) +} From 5b5d099abd32a770541f8e2a6bcd567c9b361481 Mon Sep 17 00:00:00 2001 From: Abin Simon Date: Tue, 10 May 2022 09:28:31 +0530 Subject: [PATCH 2/2] Improve auth service --- pkg/auth/v3/core.go | 132 +++++++++++++++++ pkg/auth/v3/middleware.go | 289 +++++++++++++++----------------------- pkg/auth/v3/service.go | 128 +---------------- 3 files changed, 252 insertions(+), 297 deletions(-) create mode 100644 pkg/auth/v3/core.go diff --git a/pkg/auth/v3/core.go b/pkg/auth/v3/core.go new file mode 100644 index 0000000..ed0148b --- /dev/null +++ b/pkg/auth/v3/core.go @@ -0,0 +1,132 @@ +package authv3 + +import ( + "context" + "crypto/md5" + "encoding/base64" + "errors" + "strings" + + rpcv3 "github.com/RafayLabs/rcloud-base/proto/rpc/user" + authzv1 "github.com/RafayLabs/rcloud-base/proto/types/authz" + commonv3 "github.com/RafayLabs/rcloud-base/proto/types/commonpb/v3" +) + +var ( + // ErrInvalidAPIKey is returned when api key is invalid + ErrInvalidAPIKey = errors.New("invalid api key") + // ErrInvalidSignature is returns when signature is invalid + ErrInvalidSignature = errors.New("invalid signature") +) + +func (ac *authContext) IsRequestAllowed(ctx context.Context, req *commonv3.IsRequestAllowedRequest) (*commonv3.IsRequestAllowedResponse, error) { + res := &commonv3.IsRequestAllowedResponse{ + Status: commonv3.RequestStatus_Unknown, + SessionData: &commonv3.SessionData{}, + } + + // Authenticate request + succ, err := ac.authenticate(ctx, req, res) + if err != nil { + return nil, err + } + // Don't bother checking authorization if athentication failed + if !succ { + return res, nil + } + + if req.NoAuthz { + return res, nil + } + + // Authorize request + err = ac.authorize(ctx, req, res) + if err != nil { + return nil, err + } + + return res, nil +} + +func getTokenCheckSum(body []byte) string { + hash := md5.New() + hash.Write(body) + return base64.StdEncoding.EncodeToString(hash.Sum(nil)) +} + +// authenticate validate whether the request is from a legitimate user +// and populate relevant information in res. +func (ac *authContext) authenticate(ctx context.Context, req *commonv3.IsRequestAllowedRequest, res *commonv3.IsRequestAllowedResponse) (bool, error) { + if len(req.XApiKey) > 0 && len(req.XSessionToken) == 0 { + resp, err := ac.ks.GetByKey(ctx, &rpcv3.ApiKeyRequest{ + Id: req.XApiKey, + }) + if err != nil { + _log.Infow("unable to get api key", "key", req.XApiKey, "error", err) + return false, ErrInvalidAPIKey + } + if !(req.XApiToken == getTokenCheckSum([]byte(resp.Secret))) { + return false, ErrInvalidSignature + } + _log.Info("successfully validated api key ", req.XApiKey) + res.Status = commonv3.RequestStatus_RequestAllowed + res.SessionData.Username = resp.Name + res.SessionData.Account = resp.AccountID.String() + } else { + tsr := ac.kc.V0alpha2Api.ToSession(ctx). + XSessionToken(req.GetXSessionToken()). + Cookie(req.GetCookie()) + session, _, err := ac.kc.V0alpha2Api.ToSessionExecute(tsr) + if err != nil { + // '401 Unauthorized' if the credentials are invalid or no credentials were sent. + if strings.Contains(err.Error(), "401 Unauthorized") { + res.Status = commonv3.RequestStatus_RequestNotAuthenticated + res.Reason = "no or invalid credentials" + return false, nil + } else { + return false, err + } + } + if session.GetActive() { + res.Status = commonv3.RequestStatus_RequestAllowed + res.SessionData.Account = session.Identity.GetId() + + t := session.Identity.Traits.(map[string]interface{}) + res.SessionData.Username = t["email"].(string) + } else { + res.Status = commonv3.RequestStatus_RequestNotAuthenticated + res.Reason = "no active session" + } + } + return true, nil +} + +// authorize performs authorization of the request +func (ac *authContext) authorize(ctx context.Context, req *commonv3.IsRequestAllowedRequest, res *commonv3.IsRequestAllowedResponse) error { + // user,namespace,project,org,url(perm),method + // ones that don't have value should be "*" + proj := req.Project + if proj == "" { + proj = "*" + } + org := req.Org + if org == "" { + org = "*" + } + er := authzv1.EnforceRequest{ + Params: []string{"u:" + res.SessionData.Username, "*", proj, org, req.Url, req.Method}, + } + authenticated, err := ac.as.Enforce(ctx, &er) + if err != nil { + return err + } + if !authenticated.Res { + res.Status = commonv3.RequestStatus_RequestMethodOrURLNotAllowed + res.Reason = "not authorized to perform action" + return nil + } + + // the following would already be set in auth, but just in case + res.Status = commonv3.RequestStatus_RequestAllowed + return nil +} diff --git a/pkg/auth/v3/middleware.go b/pkg/auth/v3/middleware.go index 1dd16f6..78bd898 100644 --- a/pkg/auth/v3/middleware.go +++ b/pkg/auth/v3/middleware.go @@ -25,14 +25,9 @@ type authMiddleware struct { opt Option } -type remoteAuthMiddleware struct { - as rpcv3.AuthClient - db *bun.DB - opt Option -} - +// NewAuthMiddleware creates as a middleware for the HTTP server which +// does the auth and authz by talking to kratos server and casbin func NewAuthMiddleware(al *zap.Logger, opt Option) negroni.Handler { - // Initialize database sqldb := sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(getDSN()))) return &authMiddleware{ ac: SetupAuthContext(al), @@ -41,6 +36,15 @@ func NewAuthMiddleware(al *zap.Logger, opt Option) negroni.Handler { } } +type remoteAuthMiddleware struct { + as rpcv3.AuthClient + db *bun.DB + opt Option +} + +// NewRemoteAuthMiddleware creates a middleware for the HTTP server +// which does auth and authz by talking to the auth service exposed by +// rcloud-base via grpc. func NewRemoteAuthMiddleware(al *zap.Logger, as string, opt Option) negroni.Handler { sqldb := sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(getDSN()))) conn, err := grpc.Dial(as, grpc.WithInsecure()) @@ -56,173 +60,114 @@ func NewRemoteAuthMiddleware(al *zap.Logger, as string, opt Option) negroni.Hand } } +func serveHTTP(opt Option, + db *bun.DB, + isRequestAllowed func(context.Context, *commonpbv3.IsRequestAllowedRequest) (*commonpbv3.IsRequestAllowedResponse, error), + rw http.ResponseWriter, + r *http.Request, + next http.HandlerFunc, +) { + for _, ex := range opt.ExcludeURLs { + match, err := regexp.MatchString(ex, r.URL.Path) + if err != nil { + _log.Errorf("failed to match URL expression", err) + http.Error(rw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + if match { + next(rw, r) + return + } + } + // Auth is primarily done via grpc endpoints, this is only used + // for endoints which do not go through grpc As of now, it is just + // prompt. + var poResp dao.ProjectOrg + + if strings.HasPrefix(r.URL.String(), "/v2/debug/prompt/project/") { + // /v2/debug/prompt/project/:project/cluster/:cluster_name + splits := strings.Split(r.URL.String(), "/") + if len(splits) > 5 { + // we have to fetch the org info for casbin + res, err := dao.GetProjectOrganization(r.Context(), db, splits[5]) + if err != nil { + _log.Errorf("Failed to authenticate: unable to find project") + http.Error(rw, http.StatusText(http.StatusForbidden), http.StatusForbidden) + return + } + _log.Info("found project with organization ", res.Organization) + poResp = res + } + } else { + // The middleware to only used with routes which does not have + // a grpc and so fail for any other requests. + _log.Errorf("Failed to authenticate: not a prompt request") + http.Error(rw, http.StatusText(http.StatusForbidden), http.StatusForbidden) + return + } + + req := &commonpbv3.IsRequestAllowedRequest{ + Url: r.URL.String(), + Method: r.Method, + XSessionToken: r.Header.Get("X-Session-Token"), + XApiKey: r.Header.Get("X-API-KEYID"), + XApiToken: r.Header.Get("X-API-TOKEN"), + Cookie: r.Header.Get("Cookie"), + Project: poResp.Project, + Org: poResp.Organization, + } + res, err := isRequestAllowed(r.Context(), req) + if err != nil { + _log.Errorf("Failed to authenticate a request: %s", err) + http.Error(rw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + s := res.GetStatus() + switch s { + case commonpbv3.RequestStatus_RequestAllowed: + // update the session data response to be used within prompt + res.SessionData.Organization = poResp.OrganizationId + res.SessionData.Partner = poResp.PartnerId + res.SessionData.Project = &commonpbv3.ProjectData{ + List: []*commonpbv3.ProjectRole{ + { + ProjectId: poResp.ProjectId, + }, + }, + } + ctx := context.WithValue(r.Context(), common.SessionDataKey, res.SessionData) + next(rw, r.WithContext(ctx)) + return + case commonpbv3.RequestStatus_RequestMethodOrURLNotAllowed: + http.Error(rw, res.GetReason(), http.StatusForbidden) + return + case commonpbv3.RequestStatus_RequestNotAuthenticated: + http.Error(rw, res.GetReason(), http.StatusUnauthorized) + return + } + + // status is unknown + http.Error(rw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) +} + +// ServeHTTP function is called by the HTTP server to invoke the +// middleware func (am *authMiddleware) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { - for _, ex := range am.opt.ExcludeURLs { - match, err := regexp.MatchString(ex, r.URL.Path) - if err != nil { - _log.Errorf("failed to match URL expression", err) - http.Error(rw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - if match { - next(rw, r) - return - } - } - // Auth is primarily done via grpc endpoints, this is only used - // for endoints which do not go through grpc As of now, it is just - // prompt. - var poResp dao.ProjectOrg - - if strings.HasPrefix(r.URL.String(), "/v2/debug/prompt/project/") { - // /v2/debug/prompt/project/:project/cluster/:cluster_name - splits := strings.Split(r.URL.String(), "/") - if len(splits) > 5 { - // we have to fetch the org info for casbin - res, err := dao.GetProjectOrganization(r.Context(), am.db, splits[5]) - if err != nil { - _log.Errorf("Failed to authenticate: unable to find project") - http.Error(rw, http.StatusText(http.StatusForbidden), http.StatusForbidden) - return - } - _log.Info("found project with organization ", res.Organization) - poResp = res - } - } else { - // The middleware to only used with routes which does not have - // a grpc and so fail for any other requests. - _log.Errorf("Failed to authenticate: not a prompt request") - http.Error(rw, http.StatusText(http.StatusForbidden), http.StatusForbidden) - return - } - - req := &commonpbv3.IsRequestAllowedRequest{ - Url: r.URL.String(), - Method: r.Method, - XSessionToken: r.Header.Get("X-Session-Token"), - XApiKey: r.Header.Get("X-API-KEYID"), - XApiToken: r.Header.Get("X-API-TOKEN"), - Cookie: r.Header.Get("Cookie"), - Project: poResp.Project, - Org: poResp.Organization, - } - res, err := am.ac.IsRequestAllowed(r.Context(), req) - if err != nil { - _log.Errorf("Failed to authenticate a request: %s", err) - http.Error(rw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - - s := res.GetStatus() - switch s { - case commonpbv3.RequestStatus_RequestAllowed: - // update the session data response to be used within prompt - res.SessionData.Organization = poResp.OrganizationId - res.SessionData.Partner = poResp.PartnerId - res.SessionData.Project = &commonpbv3.ProjectData{ - List: []*commonpbv3.ProjectRole{ - { - ProjectId: poResp.ProjectId, - }, - }, - } - ctx := context.WithValue(r.Context(), common.SessionDataKey, res.SessionData) - next(rw, r.WithContext(ctx)) - return - case commonpbv3.RequestStatus_RequestMethodOrURLNotAllowed: - http.Error(rw, res.GetReason(), http.StatusForbidden) - return - case commonpbv3.RequestStatus_RequestNotAuthenticated: - http.Error(rw, res.GetReason(), http.StatusUnauthorized) - return - } - - // status is unknown - http.Error(rw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + serveHTTP(am.opt, am.db, am.ac.IsRequestAllowed, rw, r, next) } -// Same as previous ServeHTTP, just using remoteAuthMiddleware instead of authMiddleware +// ServeHTTP function is called by the HTTP server to invoke the +// middleware. Same as previous ServeHTTP, but uses +// remoteAuthMiddleware instead of authMiddleware func (am *remoteAuthMiddleware) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { - for _, ex := range am.opt.ExcludeURLs { - match, err := regexp.MatchString(ex, r.URL.Path) - if err != nil { - _log.Errorf("failed to match URL expression", err) - http.Error(rw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - if match { - next(rw, r) - return - } - } - // Auth is primarily done via grpc endpoints, this is only used - // for endoints which do not go through grpc As of now, it is just - // prompt. - var poResp dao.ProjectOrg - - if strings.HasPrefix(r.URL.String(), "/v2/debug/prompt/project/") { - // /v2/debug/prompt/project/:project/cluster/:cluster_name - splits := strings.Split(r.URL.String(), "/") - if len(splits) > 5 { - // we have to fetch the org info for casbin - res, err := dao.GetProjectOrganization(r.Context(), am.db, splits[5]) - if err != nil { - _log.Errorf("Failed to authenticate: unable to find project") - http.Error(rw, http.StatusText(http.StatusForbidden), http.StatusForbidden) - return - } - _log.Info("found project with organization ", res.Organization) - poResp = res - } - } else { - // The middleware to only used with routes which does not have - // a grpc and so fail for any other requests. - _log.Errorf("Failed to authenticate: not a prompt request") - http.Error(rw, http.StatusText(http.StatusForbidden), http.StatusForbidden) - return - } - - req := &commonpbv3.IsRequestAllowedRequest{ - Url: r.URL.String(), - Method: r.Method, - XSessionToken: r.Header.Get("X-Session-Token"), - XApiKey: r.Header.Get("X-API-KEYID"), - XApiToken: r.Header.Get("X-API-TOKEN"), - Cookie: r.Header.Get("Cookie"), - Project: poResp.Project, - Org: poResp.Organization, - } - res, err := am.as.IsRequestAllowed(r.Context(), req) - if err != nil { - _log.Errorf("Failed to authenticate a request: %s", err) - http.Error(rw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - - s := res.GetStatus() - switch s { - case commonpbv3.RequestStatus_RequestAllowed: - // update the session data response to be used within prompt - res.SessionData.Organization = poResp.OrganizationId - res.SessionData.Partner = poResp.PartnerId - res.SessionData.Project = &commonpbv3.ProjectData{ - List: []*commonpbv3.ProjectRole{ - { - ProjectId: poResp.ProjectId, - }, - }, - } - ctx := context.WithValue(r.Context(), common.SessionDataKey, res.SessionData) - next(rw, r.WithContext(ctx)) - return - case commonpbv3.RequestStatus_RequestMethodOrURLNotAllowed: - http.Error(rw, res.GetReason(), http.StatusForbidden) - return - case commonpbv3.RequestStatus_RequestNotAuthenticated: - http.Error(rw, res.GetReason(), http.StatusUnauthorized) - return - } - - // status is unknown - http.Error(rw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + serveHTTP( + am.opt, + am.db, + func(ctx context.Context, isr *commonpbv3.IsRequestAllowedRequest) (*commonpbv3.IsRequestAllowedResponse, error) { + return am.as.IsRequestAllowed(ctx, isr) + }, + rw, + r, + next) } diff --git a/pkg/auth/v3/service.go b/pkg/auth/v3/service.go index 28339f8..b49993a 100644 --- a/pkg/auth/v3/service.go +++ b/pkg/auth/v3/service.go @@ -2,23 +2,10 @@ package authv3 import ( "context" - "crypto/md5" - "encoding/base64" - "errors" - "strings" - rpcv3 "github.com/RafayLabs/rcloud-base/proto/rpc/user" - authzv1 "github.com/RafayLabs/rcloud-base/proto/types/authz" commonv3 "github.com/RafayLabs/rcloud-base/proto/types/commonpb/v3" ) -var ( - // ErrInvalidAPIKey is returned when api key is invalid - ErrInvalidAPIKey = errors.New("invalid api key") - // ErrInvalidSignature is returns when signature is invalid - ErrInvalidSignature = errors.New("invalid signature") -) - type authService struct { ac authContext } @@ -31,118 +18,9 @@ func NewAuthService(ac authContext) AuthService { return &authService{ac} } +// Auth is exposed as an external service so that other modules like +// prompt can call into this inorder to authenticate. This will be +// made use of using `remoteAuthMiddleware` in other services. func (s *authService) IsRequestAllowed(ctx context.Context, req *commonv3.IsRequestAllowedRequest) (*commonv3.IsRequestAllowedResponse, error) { return s.ac.IsRequestAllowed(ctx, req) } - -func (ac *authContext) IsRequestAllowed(ctx context.Context, req *commonv3.IsRequestAllowedRequest) (*commonv3.IsRequestAllowedResponse, error) { - res := &commonv3.IsRequestAllowedResponse{ - Status: commonv3.RequestStatus_Unknown, - SessionData: &commonv3.SessionData{}, - } - - // Authenticate request - succ, err := ac.authenticate(ctx, req, res) - if err != nil { - return nil, err - } - // Don't bother checking authorization if athentication failed - if !succ { - return res, nil - } - - if req.NoAuthz { - return res, nil - } - - // Authorize request - err = ac.authorize(ctx, req, res) - if err != nil { - return nil, err - } - - return res, nil -} - -func getTokenCheckSum(body []byte) string { - hash := md5.New() - hash.Write(body) - return base64.StdEncoding.EncodeToString(hash.Sum(nil)) -} - -// authenticate validate whether the request is from a legitimate user -// and populate relevant information in res. -func (ac *authContext) authenticate(ctx context.Context, req *commonv3.IsRequestAllowedRequest, res *commonv3.IsRequestAllowedResponse) (bool, error) { - if len(req.XApiKey) > 0 && len(req.XSessionToken) == 0 { - resp, err := ac.ks.GetByKey(ctx, &rpcv3.ApiKeyRequest{ - Id: req.XApiKey, - }) - if err != nil { - _log.Infow("unable to get api key", "key", req.XApiKey, "error", err) - return false, ErrInvalidAPIKey - } - if !(req.XApiToken == getTokenCheckSum([]byte(resp.Secret))) { - return false, ErrInvalidSignature - } - _log.Info("successfully validated api key ", req.XApiKey) - res.Status = commonv3.RequestStatus_RequestAllowed - res.SessionData.Username = resp.Name - res.SessionData.Account = resp.AccountID.String() - } else { - tsr := ac.kc.V0alpha2Api.ToSession(ctx). - XSessionToken(req.GetXSessionToken()). - Cookie(req.GetCookie()) - session, _, err := ac.kc.V0alpha2Api.ToSessionExecute(tsr) - if err != nil { - // '401 Unauthorized' if the credentials are invalid or no credentials were sent. - if strings.Contains(err.Error(), "401 Unauthorized") { - res.Status = commonv3.RequestStatus_RequestNotAuthenticated - res.Reason = "no or invalid credentials" - return false, nil - } else { - return false, err - } - } - if session.GetActive() { - res.Status = commonv3.RequestStatus_RequestAllowed - res.SessionData.Account = session.Identity.GetId() - - t := session.Identity.Traits.(map[string]interface{}) - res.SessionData.Username = t["email"].(string) - } else { - res.Status = commonv3.RequestStatus_RequestNotAuthenticated - res.Reason = "no active session" - } - } - return true, nil -} - -// authorize performs authorization of the request -func (ac *authContext) authorize(ctx context.Context, req *commonv3.IsRequestAllowedRequest, res *commonv3.IsRequestAllowedResponse) error { - // user,namespace,project,org,url(perm),method - // ones that don't have value should be "*" - proj := req.Project - if proj == "" { - proj = "*" - } - org := req.Org - if org == "" { - org = "*" - } - er := authzv1.EnforceRequest{ - Params: []string{"u:" + res.SessionData.Username, "*", proj, org, req.Url, req.Method}, - } - authenticated, err := ac.as.Enforce(ctx, &er) - if err != nil { - return err - } - if !authenticated.Res { - res.Status = commonv3.RequestStatus_RequestMethodOrURLNotAllowed - res.Reason = "not authorized to perform action" - return nil - } - - // the following would already be set in auth, but just in case - res.Status = commonv3.RequestStatus_RequestAllowed - return nil -}