Merge pull request #132 from RafayLabs/remote-auth

Add auth service
This commit is contained in:
Abin Simon
2022-05-10 10:44:21 +05:30
committed by GitHub
6 changed files with 236 additions and 126 deletions

10
main.go
View File

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

132
pkg/auth/v3/core.go Normal file
View File

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

View File

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

View File

@@ -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,8 +25,9 @@ type authMiddleware struct {
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),
@@ -33,8 +36,38 @@ func NewAuthMiddleware(al *zap.Logger, opt Option) negroni.Handler {
}
}
func (am *authMiddleware) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
for _, ex := range am.opt.ExcludeURLs {
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())
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 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)
@@ -56,7 +89,7 @@ func (am *authMiddleware) ServeHTTP(rw http.ResponseWriter, r *http.Request, nex
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])
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)
@@ -83,7 +116,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 := 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 +126,7 @@ 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{
@@ -117,3 +150,24 @@ func (am *authMiddleware) ServeHTTP(rw http.ResponseWriter, r *http.Request, nex
// 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) {
serveHTTP(am.opt, am.db, am.ac.IsRequestAllowed, rw, r, next)
}
// 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) {
serveHTTP(
am.opt,
am.db,
func(ctx context.Context, isr *commonpbv3.IsRequestAllowedRequest) (*commonpbv3.IsRequestAllowedResponse, error) {
return am.as.IsRequestAllowed(ctx, isr)
},
rw,
r,
next)
}

View File

@@ -2,133 +2,25 @@ package authv3
import (
"context"
"crypto/md5"
"encoding/base64"
"errors"
"net/http"
"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, httpreq *http.Request, 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)
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
type authService struct {
ac authContext
}
func getTokenCheckSum(body []byte) string {
hash := md5.New()
hash.Write(body)
return base64.StdEncoding.EncodeToString(hash.Sum(nil))
type AuthService interface {
IsRequestAllowed(context.Context, *commonv3.IsRequestAllowedRequest) (*commonv3.IsRequestAllowedResponse, error)
}
// 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) {
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()
// TODO: Better way to access traits
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
func NewAuthService(ac authContext) AuthService {
return &authService{ac}
}
// 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
// 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)
}

22
server/auth.go Normal file
View File

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