Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,15 @@ require (
github.com/krolaw/dhcp4 v0.0.0-20190909130307-a50d88189771
github.com/pin/tftp v2.1.0+incompatible
golang.org/x/crypto v0.30.0
golang.org/x/net v0.21.0
golang.org/x/net v0.25.0
golang.org/x/sys v0.28.0
google.golang.org/grpc v1.65.0
)

require (
github.com/jmespath/go-jmespath v0.4.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/term v0.27.0 // indirect
golang.org/x/text v0.21.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect
google.golang.org/protobuf v1.34.2 // indirect
)
21 changes: 11 additions & 10 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
Expand All @@ -22,30 +24,29 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b h1:huxqepDufQpLLIRXiVkTvnxrzJlpwmIWAObmcCcUFr0=
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY=
golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20221004154528-8021a29435af h1:wv66FM3rLZGPdxpYL+ApnDe2HzHcTFta3z5nsc13wI4=
golang.org/x/net v0.0.0-20221004154528-8021a29435af/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 h1:Zy9XzmMEflZ/MAaA7vNcoebnRAld7FsPW1EeBB7V0m8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
259 changes: 259 additions & 0 deletions lib/grpc/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
package grpc

import (
"context"
"strings"
"time"

"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/peer"
"google.golang.org/grpc/status"

"github.com/Cloud-Foundations/Dominator/lib/srpc"
)

type connKeyType struct{}

var connKey = connKeyType{}

var publicMethods = make(map[string]struct{})
var unauthenticatedMethods = make(map[string]struct{})

// grpcToSrpcMethodMapping maps gRPC method names to SRPC method names per service.
var grpcToSrpcMethodMapping = make(map[string]map[string]string)

// Interface check.
var _ srpc.AuthConn = (*Conn)(nil)

// Conn holds authentication information for gRPC handlers.
type Conn struct {
authInfo *srpc.AuthInformation
permittedMethods map[string]struct{}
}

// GetAuthInformation returns the authentication information or nil.
func (c *Conn) GetAuthInformation() *srpc.AuthInformation {
if c == nil {
return nil
}
return c.authInfo
}

func (c *Conn) GetPermittedMethods() map[string]struct{} {
if c == nil {
return nil
}
return c.permittedMethods
}

// AllowMethodPowers always returns true for gRPC. SRPC supports a
// "doNotUseMethodPowers" query parameter allowing clients to opt-out of
// method powers; gRPC has no equivalent mechanism.
func (c *Conn) AllowMethodPowers() bool {
return true
}

func authorizeRequest(ctx context.Context, fullMethod string) (context.Context, error) {
_, isPublic := publicMethods[fullMethod]
_, isUnauthenticated := unauthenticatedMethods[fullMethod]

if isUnauthenticated {
return ContextWithConn(ctx, &Conn{}), nil
}

conn, err := buildAuthConn(ctx)
if err != nil {
return nil, err
}
if conn.GetAuthInformation() == nil {
return nil, status.Error(codes.Unauthenticated, "no auth information")
}

srpcMethod := grpcToSrpcMethod(fullMethod)
authorized, haveMethodAccess := srpc.CheckAuthorization(srpcMethod, conn,
srpc.GetDefaultGrantMethod(), isPublic, false)
if !authorized {
recordDeniedCall(fullMethod)
return nil, status.Error(codes.PermissionDenied, "call on "+fullMethod)
}
conn.GetAuthInformation().HaveMethodAccess = haveMethodAccess
return ContextWithConn(ctx, conn), nil
}

// UnaryAuthInterceptor handles authentication and authorization for unary RPCs.
func UnaryAuthInterceptor(ctx context.Context, req interface{},
info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {

ctx, err := authorizeRequest(ctx, info.FullMethod)
if err != nil {
return nil, err
}

recordCallStart()
startTime := time.Now()
defer func() {
if r := recover(); r != nil {
recordPanic()
panic(r)
}
}()
resp, err := handler(ctx, req)
recordCallEnd(info.FullMethod, startTime, err)
return resp, err
}

// StreamAuthInterceptor handles authentication and authorization for streaming RPCs.
func StreamAuthInterceptor(srv interface{}, ss grpc.ServerStream,
info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {

ctx, err := authorizeRequest(ss.Context(), info.FullMethod)
if err != nil {
return err
}

wrapped := &wrappedStream{ServerStream: ss, ctx: ctx}
recordCallStart()
startTime := time.Now()
defer func() {
if r := recover(); r != nil {
recordPanic()
panic(r)
}
}()
err = handler(srv, wrapped)
recordCallEnd(info.FullMethod, startTime, err)
return err
}

// ConnFromContext returns the Conn from the context.
func ConnFromContext(ctx context.Context) *Conn {
if v := ctx.Value(connKey); v != nil {
return v.(*Conn)
}
return nil
}

// ContextWithConn returns a context with the Conn attached.
func ContextWithConn(ctx context.Context, conn *Conn) context.Context {
return context.WithValue(ctx, connKey, conn)
}

// ServiceOptions configures authorization for a gRPC service.
type ServiceOptions struct {
PublicMethods []string // SRPC method names
UnauthenticatedMethods []string // SRPC method names
GrpcToSrpcMethods map[string]string // e.g., {"ListVms": "ListVMs"}
GrpcOnlyPublicMethods []string // gRPC-only public methods
GrpcOnlyUnauthenticatedMethods []string // gRPC-only unauthenticated methods
}

// RegisterServiceOptions registers public and unauthenticated methods for a service.
// It translates SRPC method names to gRPC equivalents and stores the mapping for RBAC.
func RegisterServiceOptions(serviceName string, options ServiceOptions) {
srpcToGrpc := reverseMapping(options.GrpcToSrpcMethods)

allMethods := make(map[string]struct{})
for _, method := range translateMethods(options.PublicMethods, srpcToGrpc, options.GrpcOnlyPublicMethods) {
fullMethod := "/" + serviceName + "/" + method
publicMethods[fullMethod] = struct{}{}
allMethods[fullMethod] = struct{}{}
}
for _, method := range translateMethods(options.UnauthenticatedMethods, srpcToGrpc, options.GrpcOnlyUnauthenticatedMethods) {
fullMethod := "/" + serviceName + "/" + method
unauthenticatedMethods[fullMethod] = struct{}{}
allMethods[fullMethod] = struct{}{}
}
if options.GrpcToSrpcMethods != nil {
grpcToSrpcMethodMapping[serviceName] = options.GrpcToSrpcMethods
}
registerMethodMetrics(serviceName, allMethods)
}

func reverseMapping(m map[string]string) map[string]string {
if m == nil {
return nil
}
result := make(map[string]string, len(m))
for k, v := range m {
result[v] = k
}
return result
}

func translateMethods(srpcMethods []string, srpcToGrpc map[string]string, grpcOnly []string) []string {
result := make([]string, 0, len(srpcMethods)+len(grpcOnly))
for _, method := range srpcMethods {
if mapped, ok := srpcToGrpc[method]; ok {
result = append(result, mapped)
} else {
result = append(result, method)
}
}
return append(result, grpcOnly...)
}

// grpcToSrpcMethod converts "/hypervisor.Hypervisor/ListVms" to "Hypervisor.ListVMs".
func grpcToSrpcMethod(fullMethod string) string {
parts := strings.Split(strings.TrimPrefix(fullMethod, "/"), "/")
if len(parts) != 2 {
return fullMethod
}
servicePart := parts[0]
methodName := parts[1]

serviceParts := strings.Split(servicePart, ".")
serviceName := serviceParts[len(serviceParts)-1]

if methodMap, ok := grpcToSrpcMethodMapping[servicePart]; ok {
if srpcMethod, ok := methodMap[methodName]; ok {
methodName = srpcMethod
}
}

return serviceName + "." + methodName
}

func buildAuthConn(ctx context.Context) (*Conn, error) {
p, ok := peer.FromContext(ctx)
if !ok {
return nil, status.Error(codes.Unauthenticated, "no peer info in context")
}

if p.AuthInfo == nil {
return nil, status.Error(codes.Unauthenticated, "no TLS auth info")
}
tlsInfo, ok := p.AuthInfo.(credentials.TLSInfo)
if !ok {
return nil, status.Error(codes.Unauthenticated, "unexpected auth info type")
}
username, permittedMethods, groupList, err := srpc.GetAuth(tlsInfo.State)
if err != nil {
return nil, status.Error(codes.Unauthenticated, err.Error())
}
return &Conn{
authInfo: &srpc.AuthInformation{
Username: username,
GroupList: groupList,
},
permittedMethods: permittedMethods,
}, nil
}

// wrappedStream overrides Context to include auth info.
type wrappedStream struct {
grpc.ServerStream
ctx context.Context
}

func (w *wrappedStream) Context() context.Context {
return w.ctx
}

// Metrics stubs - replaced by metrics.go in a later PR.
func recordDeniedCall(fullMethod string) {}
func recordCallStart() {}
func recordCallEnd(fullMethod string, startTime time.Time, err error) {}
func recordPanic() {}
func registerMethodMetrics(serviceName string, methods map[string]struct{}) {}
22 changes: 22 additions & 0 deletions lib/srpc/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,13 @@ type Encoder interface {
Encode(e interface{}) error
}

// AuthConn defines the interface for authorization checks.
type AuthConn interface {
GetAuthInformation() *AuthInformation
GetPermittedMethods() map[string]struct{}
AllowMethodPowers() bool
}

type FakeClientOptions struct{}

// MethodBlocker defines an interface to block method calls (after possible
Expand Down Expand Up @@ -269,6 +276,11 @@ func SetDefaultGrantMethod(grantMethod func(serviceMethod string,
defaultGrantMethod = grantMethod
}

// GetDefaultGrantMethod returns the default grant method for all receivers.
func GetDefaultGrantMethod() func(serviceMethod string, authInfo *AuthInformation) bool {
return defaultGrantMethod
}

// SetDefaultLogger will override the default logger used.
func SetDefaultLogger(l log.DebugLogger) {
logger = l
Expand Down Expand Up @@ -540,6 +552,16 @@ func (conn *Conn) Username() string {
return conn.getUsername()
}

// GetPermittedMethods returns the methods permitted by the client certificate.
func (conn *Conn) GetPermittedMethods() map[string]struct{} {
return conn.permittedMethods
}

// AllowMethodPowers returns true if the client has method powers.
func (conn *Conn) AllowMethodPowers() bool {
return conn.allowMethodPowers
}

type ReceiverOptions struct {
PublicMethods []string // Methods not requiring method powers.
UnauthenticatedMethods []string // Methods not requiring authentication.
Expand Down
Loading