Serge Bazanski | 3379a5d | 2021-09-09 12:56:40 +0200 | [diff] [blame] | 1 | package rpc |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "regexp" |
| 6 | |
| 7 | "google.golang.org/grpc/codes" |
| 8 | "google.golang.org/grpc/status" |
| 9 | "google.golang.org/protobuf/proto" |
| 10 | "google.golang.org/protobuf/reflect/protoreflect" |
| 11 | "google.golang.org/protobuf/reflect/protoregistry" |
| 12 | |
| 13 | epb "source.monogon.dev/metropolis/proto/ext" |
| 14 | ) |
| 15 | |
| 16 | // methodInfo is the parsed information for a given RPC method, as configured by |
| 17 | // the metropolis.common.ext.authorization extension. |
| 18 | type methodInfo struct { |
| 19 | // unauthenticated is true if the method is defined as 'unauthenticated', ie. |
| 20 | // that all requests should be passed to the gRPC handler without any |
| 21 | // authentication or authorization performed. |
| 22 | unauthenticated bool |
| 23 | // need is a map of permissions that the caller needs to have in order to be |
| 24 | // allowed to call this method. If not empty, unauthenticated cannot be set to |
| 25 | // true. |
| 26 | need map[epb.Permission]bool |
| 27 | } |
| 28 | |
| 29 | var ( |
| 30 | // reMethodName matches a /some.service/Method string from |
| 31 | // {Stream,Unary}ServerInfo.FullMethod. |
| 32 | reMethodName = regexp.MustCompile(`^/([^/]+)/([^/.]+)$`) |
| 33 | ) |
| 34 | |
| 35 | // getMethodInfo returns the methodInfo for a given method name, as retrieved |
| 36 | // from grpc.{Stream,Unary}ServerInfo.FullMethod, or nil if the method could not |
| 37 | // be found. |
| 38 | // |
| 39 | // SECURITY: If the given method does not have any |
| 40 | // metropolis.common.ext.authorization annotations, a methodInfo which requires |
| 41 | // authorization but no permissions is returned, defaulting to a mildly secure |
| 42 | // default of a method that can be called by any authenticated user. |
| 43 | func getMethodInfo(methodName string) (*methodInfo, error) { |
| 44 | m := reMethodName.FindStringSubmatch(methodName) |
| 45 | if len(m) != 3 { |
| 46 | return nil, status.Errorf(codes.InvalidArgument, "invalid method name %q", methodName) |
| 47 | } |
| 48 | // Convert /foo.bar/Method to foo.bar.Method, which is used by the protoregistry. |
| 49 | methodName = fmt.Sprintf("%s.%s", m[1], m[2]) |
| 50 | desc, err := protoregistry.GlobalFiles.FindDescriptorByName(protoreflect.FullName(methodName)) |
| 51 | if err != nil { |
| 52 | return nil, status.Errorf(codes.InvalidArgument, "could not retrieve descriptor for method: %v", err) |
| 53 | } |
| 54 | method, ok := desc.(protoreflect.MethodDescriptor) |
| 55 | if !ok { |
| 56 | return nil, status.Error(codes.InvalidArgument, "querying method name did not yield a MethodDescriptor") |
| 57 | } |
| 58 | |
| 59 | // Get authorization extension, defaults to no options set. |
| 60 | if !proto.HasExtension(method.Options(), epb.E_Authorization) { |
| 61 | return nil, status.Errorf(codes.Internal, "method does not provide Authorization extension, failing safe") |
| 62 | } |
| 63 | authz, ok := proto.GetExtension(method.Options(), epb.E_Authorization).(*epb.Authorization) |
| 64 | if !ok { |
| 65 | return nil, status.Errorf(codes.Internal, "method contains Authorization extension with wrong type, failing safe") |
| 66 | } |
| 67 | if authz == nil { |
| 68 | return nil, status.Errorf(codes.Internal, "method contains nil Authorization extension, failing safe") |
| 69 | } |
| 70 | |
| 71 | // If unauthenticated connections are allowed, return immediately. |
| 72 | if authz.AllowUnauthenticated && len(authz.Need) == 0 { |
| 73 | return &methodInfo{ |
| 74 | unauthenticated: true, |
| 75 | }, nil |
| 76 | } |
| 77 | |
| 78 | // Otherwise, return needed permissions. |
| 79 | res := &methodInfo{ |
| 80 | need: make(map[epb.Permission]bool), |
| 81 | } |
| 82 | for _, n := range authz.Need { |
| 83 | res.need[n] = true |
| 84 | } |
| 85 | return res, nil |
| 86 | } |