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