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  }