diff --git a/pkg/auth/v3/auth.go b/pkg/auth/v3/auth.go index 9565b89..1ec9c17 100644 --- a/pkg/auth/v3/auth.go +++ b/pkg/auth/v3/auth.go @@ -6,8 +6,11 @@ import ( "github.com/RafayLabs/rcloud-base/pkg/enforcer" logv2 "github.com/RafayLabs/rcloud-base/pkg/log" "github.com/RafayLabs/rcloud-base/pkg/service" + "github.com/RafayLabs/rcloud-base/pkg/enforcer" kclient "github.com/ory/kratos-client-go" "github.com/uptrace/bun" + "gorm.io/driver/postgres" + "gorm.io/gorm" ) var _log = logv2.GetLogger() @@ -27,6 +30,7 @@ type Option struct { type authContext struct { kc *kclient.APIClient ks service.ApiKeyService + as service.AuthzService } // NewAuthContext setup authentication and authorization dependencies. @@ -36,6 +40,8 @@ func NewAuthContext(db *bun.DB) authContext { kratosScheme string kratosAddr string ) + // TODO: https://github.com/RafayLabs/prompt/pull/3#issuecomment-1073557206 + // Where exactly should we be getting these values from? if v, ok := os.LookupEnv("KRATOS_SCHEME"); ok { kratosScheme = v } else { @@ -51,5 +57,18 @@ func NewAuthContext(db *bun.DB) authContext { kratosConfig.Servers[0].URL = kratosScheme + "://" + kratosAddr kc = kclient.NewAPIClient(kratosConfig) - return authContext{kc: kc, ks: service.NewApiKeyService(db)} + gormDb, err := gorm.Open( + postgres.New(postgres.Config{Conn: db.DB}), + &gorm.Config{}, + ) + if err != nil { + _log.Fatalw("unable to create db connection", "error", err) + } + enforcer, err := enforcer.NewCasbinEnforcer(gormDb).Init() + if err != nil { + _log.Fatalw("unable to init enforcer", "error", err) + } + as := service.NewAuthzService(db, enforcer) + + return authContext{kc: kc, as: as, ks: service.NewApiKeyService(db)} } diff --git a/pkg/auth/v3/interceptor.go b/pkg/auth/v3/interceptor.go index 628fbb7..f8875f1 100644 --- a/pkg/auth/v3/interceptor.go +++ b/pkg/auth/v3/interceptor.go @@ -2,9 +2,11 @@ package authv3 import ( context "context" + "reflect" + "strings" "github.com/RafayLabs/rcloud-base/pkg/gateway" - commonpbv3 "github.com/RafayLabs/rcloud-base/proto/types/commonpb/v3" + commonv3 "github.com/RafayLabs/rcloud-base/proto/types/commonpb/v3" grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" @@ -21,6 +23,27 @@ func (ac authContext) NewAuthUnaryInterceptor(opt Option) grpc.UnaryServerInterc } } + // We have to get the value of org, and project (namespace in + // future) as we will be using this inorder to authorize the + // user's access to different resources + reqValue := reflect.ValueOf(req).Elem() + field := reqValue.FieldByName("Metadata") + var org string + var project string + if field != (reflect.Value{}) { + org = field.Interface().(*commonv3.Metadata).Organization + project = field.Interface().(*commonv3.Metadata).Project + } + + // overrides for picking up info when not in default metadata locations + // XXX: This requires any new items which does not follow metadata convention added here + switch strings.Split(info.FullMethod, "/")[1] { + case "rafay.dev.rpc.v3.Project": + project = field.Interface().(*commonv3.Metadata).Name + case "rafay.dev.rpc.v3.Organization": + org = field.Interface().(*commonv3.Metadata).Name + } + md, ok := metadata.FromIncomingContext(ctx) if !ok { return nil, status.Error(codes.InvalidArgument, "grpc metadata not exist") @@ -43,11 +66,13 @@ func (ac authContext) NewAuthUnaryInterceptor(opt Option) grpc.UnaryServerInterc if len(md.Get("grpcgateway-cookie")) != 0 { cookie = md.Get("grpcgateway-cookie")[0] } - acReq := &commonpbv3.IsRequestAllowedRequest{ + acReq := &commonv3.IsRequestAllowedRequest{ Url: url, Method: method, XSessionToken: token, Cookie: cookie, + Org: org, + Project: project, } res, err := ac.IsRequestAllowed(ctx, nil, acReq) if err != nil { @@ -57,12 +82,12 @@ func (ac authContext) NewAuthUnaryInterceptor(opt Option) grpc.UnaryServerInterc s := res.GetStatus() switch s { - case commonpbv3.RequestStatus_RequestAllowed: + case commonv3.RequestStatus_RequestAllowed: ctx := NewSessionContext(ctx, res.SessionData) return handler(ctx, req) - case commonpbv3.RequestStatus_RequestMethodOrURLNotAllowed: + case commonv3.RequestStatus_RequestMethodOrURLNotAllowed: return nil, status.Error(codes.PermissionDenied, res.GetReason()) - case commonpbv3.RequestStatus_RequestNotAuthenticated: + case commonv3.RequestStatus_RequestNotAuthenticated: return nil, status.Error(codes.Unauthenticated, res.GetReason()) } diff --git a/pkg/auth/v3/service.go b/pkg/auth/v3/service.go index d3b9119..873a6aa 100644 --- a/pkg/auth/v3/service.go +++ b/pkg/auth/v3/service.go @@ -7,6 +7,7 @@ import ( "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" "github.com/spacemonkeygo/httpsig" ) @@ -25,10 +26,14 @@ func (ac *authContext) IsRequestAllowed(ctx context.Context, httpreq *http.Reque } // Authenticate request - err := ac.authenticate(ctx, httpreq, req, res) + err, succ := 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 + } // Authorize request err = ac.authorize(ctx, req, res) @@ -41,14 +46,14 @@ func (ac *authContext) IsRequestAllowed(ctx context.Context, httpreq *http.Reque // 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) error { +func (ac *authContext) authenticate(ctx context.Context, httpreq *http.Request, req *commonv3.IsRequestAllowedRequest, res *commonv3.IsRequestAllowedResponse) (error, bool) { 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 ErrInvalidAPIKey + return ErrInvalidAPIKey, false } var kg httpsig.KeyGetterFunc = func(id string) interface{} { return []byte(resp.Secret) @@ -58,7 +63,7 @@ func (ac *authContext) authenticate(ctx context.Context, httpreq *http.Request, verifier.SetRequiredHeaders([]string{"content-md5", "date", "host", "nonce"}) err = verifier.Verify(httpreq) if err != nil { - return ErrInvalidSignature + return ErrInvalidSignature, false } res.Status = commonv3.RequestStatus_RequestAllowed res.SessionData.Username = resp.Name @@ -73,9 +78,9 @@ func (ac *authContext) authenticate(ctx context.Context, httpreq *http.Request, if strings.Contains(err.Error(), "401 Unauthorized") { res.Status = commonv3.RequestStatus_RequestNotAuthenticated res.Reason = "no or invalid credentials" - return nil + return nil, false } else { - return err + return err, false } } if session.GetActive() { @@ -90,11 +95,33 @@ func (ac *authContext) authenticate(ctx context.Context, httpreq *http.Request, res.Reason = "no active session" } } - return nil + return nil, true } -// authorize performs authorization of the request and populate -// relevant information in res. +// 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 + } return nil } diff --git a/pkg/service/group.go b/pkg/service/group.go index 4325f27..5a1474d 100644 --- a/pkg/service/group.go +++ b/pkg/service/group.go @@ -82,7 +82,7 @@ func (s *groupService) createGroupRoleRelations(ctx context.Context, db bun.IDB, var ps []*authzv1.Policy for _, pnr := range projectNamespaceRoles { role := pnr.GetRole() - entity, err := pg.GetByName(ctx, db, role, &models.Role{}) + entity, err := dao.GetByName(ctx, db, role, &models.Role{}) if err != nil { return &userv3.Group{}, fmt.Errorf("unable to find role '%v'", role) } @@ -145,7 +145,7 @@ func (s *groupService) createGroupRoleRelations(ctx context.Context, db bun.IDB, if project == "" { return &userv3.Group{}, fmt.Errorf("no project name provided for role '%v'", roleName) } - projectId, err := pg.GetProjectId(ctx, s.db, project) + projectId, err := dao.GetProjectId(ctx, s.db, project) if err != nil { return &userv3.Group{}, fmt.Errorf("unable to find project '%v'", project) } diff --git a/pkg/service/user.go b/pkg/service/user.go index 430ab55..6044290 100644 --- a/pkg/service/user.go +++ b/pkg/service/user.go @@ -179,7 +179,7 @@ func (s *userService) createUserRoleRelations(ctx context.Context, db bun.IDB, u if project == "" { return &userv3.User{}, fmt.Errorf("no project name provided for role '%v'", roleName) } - projectId, err := pg.GetProjectId(ctx, db, project) + projectId, err := dao.GetProjectId(ctx, db, project) if err != nil { return user, fmt.Errorf("unable to find project '%v'", project) }