blob: 0a597faeaf85b837680ee19a95bf8140a1165c0d [file] [log] [blame]
Serge Bazanski3379a5d2021-09-09 12:56:40 +02001package rpc
2
3import (
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.
18type 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
29var (
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.
43func 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}