go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/auth/rpcacl/rpcacl.go (about) 1 // Copyright 2022 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package rpcacl implements a gRPC interceptor that checks per-RPC ACLs. 16 // 17 // It makes decisions purely based on the name of the called RPC method. It 18 // doesn't check request messages at all. 19 // 20 // This interceptor is useful as the very first coarse ACL check that just 21 // verifies the caller is known to the service. For simple services, that may be 22 // the only check. But most services will most likely need to make additional 23 // checks in the request handler (or another service-specific interceptor) that 24 // use data from the request message to make service-specific decisions. 25 package rpcacl 26 27 import ( 28 "context" 29 "fmt" 30 "strings" 31 32 "google.golang.org/grpc/codes" 33 "google.golang.org/grpc/status" 34 35 "go.chromium.org/luci/auth/identity" 36 "go.chromium.org/luci/common/logging" 37 "go.chromium.org/luci/grpc/grpcutil" 38 39 "go.chromium.org/luci/server/auth" 40 ) 41 42 // Map maps RPC methods to callers that are allowed to call them. 43 // 44 // Each key is either "/<service>/<method>" to indicate a single method, or 45 // "/<service>/*" to indicate all methods in a service. 46 // 47 // Values are LUCI group names with authorized callers or following special 48 // string: 49 // - rpcacl.Authenticated: any authenticated caller is authorized. 50 // - rpcacl.All: any caller at all is authorized (any authenticated caller and 51 // anonymous unauthenticated callers). 52 // 53 // Both rpcacl.All and rpcacl.Authenticated imply the method is publicly 54 // accessible (since it is not hard at all to get an authentication token 55 // representing *some* account). 56 // 57 // Note that group names will be publicly exposed in the PermissionDenied 58 // response messages. Do not use secret code names in group names. 59 type Map map[string]string 60 61 const ( 62 // Authenticated represents all authenticated (non-anonymous) callers. 63 Authenticated = "!AUTHENTICATED" 64 // All represents any caller at all, including anonymous. 65 All = "!ALL" 66 ) 67 68 // Interceptor returns a server interceptor that checks per-RPC ACLs. 69 // 70 // The mapping maps an RPC method to a set of callers that is authorized to 71 // call it. It should cover all services and methods exposed by the gRPC server. 72 // Access to undeclared services or methods will be denied with PermissionDenied 73 // error. 74 // 75 // This interceptor implements "static" authorization rules that do not change 76 // during lifetime of a server process. If you need to adjust ACLs dynamically, 77 // implement your own interceptor. 78 // 79 // Panics if mapping entries are malformed. 80 func Interceptor(mapping Map) grpcutil.UnifiedServerInterceptor { 81 type serviceMethod struct { 82 service string // full gRPC service name 83 method string // method name or `*` 84 } 85 86 // Copy the map to make sure it doesn't change under us. This configuration is 87 // static. Verify magic "!" prefix is used only for known magic entries. 88 rpcACL := make(map[serviceMethod]string, len(mapping)) 89 for key, val := range mapping { 90 // key must be "/service/method". 91 parts := strings.Split(key, "/") 92 if len(parts) != 3 || parts[0] != "" || parts[1] == "" || parts[2] == "" { 93 panic(fmt.Sprintf("unexpected key format: %q", key)) 94 } 95 service, method := parts[1], parts[2] 96 97 switch { 98 case val == "": 99 panic(fmt.Sprintf("for key %q: empty value", key)) 100 case strings.HasPrefix(val, "!") && val != Authenticated && val != All: 101 panic(fmt.Sprintf("for key %q: invalid value %q", key, val)) 102 } 103 104 rpcACL[serviceMethod{service, method}] = val 105 } 106 107 return func(ctx context.Context, fullMethod string, handler func(ctx context.Context) error) (err error) { 108 // fullMethod has form "/<service>/<method>". 109 parts := strings.Split(fullMethod, "/") 110 if len(parts) != 3 || parts[0] != "" { 111 panic(fmt.Sprintf("unexpected format of full method name: %q", fullMethod)) 112 } 113 service, method := parts[1], parts[2] 114 115 group := rpcACL[serviceMethod{service, method}] 116 if group == "" { 117 group = rpcACL[serviceMethod{service, "*"}] 118 if group == "" { 119 // If you see this error update rpcacl.Map to include your new API. 120 return status.Errorf(codes.PermissionDenied, "rpcacl: calling unrecognized method %q", fullMethod) 121 } 122 } 123 124 if group == All { 125 return handler(ctx) 126 } 127 128 if group == Authenticated { 129 if auth.CurrentIdentity(ctx).Kind() == identity.Anonymous { 130 return status.Errorf(codes.Unauthenticated, "rpcacl: unauthenticated calls are not allowed") 131 } 132 return handler(ctx) 133 } 134 135 switch ok, err := auth.IsMember(ctx, group); { 136 case err != nil: 137 logging.Errorf(ctx, "Error when checking %q when accessing %q: %s", group, fullMethod, err) 138 return status.Errorf(codes.Internal, "rpcacl: internal error when checking the ACL") 139 case ok: 140 return handler(ctx) 141 default: 142 return status.Errorf(codes.PermissionDenied, "rpcacl: %q is not a member of %q", auth.CurrentIdentity(ctx), group) 143 } 144 } 145 }