go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/logdog/appengine/coordinator/auth.go (about)

     1  // Copyright 2015 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 coordinator
    16  
    17  import (
    18  	"context"
    19  
    20  	"google.golang.org/grpc/codes"
    21  	"google.golang.org/grpc/status"
    22  
    23  	"go.chromium.org/luci/auth/identity"
    24  	"go.chromium.org/luci/common/logging"
    25  	"go.chromium.org/luci/logdog/common/types"
    26  	"go.chromium.org/luci/logdog/server/config"
    27  	"go.chromium.org/luci/server/auth"
    28  	"go.chromium.org/luci/server/auth/realms"
    29  )
    30  
    31  var (
    32  	// PermLogsCreate is a permission required for RegisterPrefix RPC.
    33  	PermLogsCreate = realms.RegisterPermission("logdog.logs.create")
    34  	// PermLogsGet is a permission required for reading individual streams.
    35  	PermLogsGet = realms.RegisterPermission("logdog.logs.get")
    36  	// PermLogsList is a permission required for listing streams in a prefix.
    37  	PermLogsList = realms.RegisterPermission("logdog.logs.list")
    38  )
    39  
    40  // PermissionDeniedErr is a generic "doesn't exist or don't have access" error.
    41  //
    42  // If the request is anonymous, it is an Unauthenticated error instead.
    43  func PermissionDeniedErr(ctx context.Context) error {
    44  	if id := auth.CurrentIdentity(ctx); id.Kind() == identity.Anonymous {
    45  		return status.Error(codes.Unauthenticated, "Authentication required.")
    46  	}
    47  	return status.Errorf(codes.PermissionDenied,
    48  		"The resource doesn't exist or you do not have permission to access it.")
    49  }
    50  
    51  // CheckPermission checks the caller has the requested permission.
    52  //
    53  // Logs the outcome inside (`prefix` is used only in this logging). Returns
    54  // gRPC errors that can be returned directly to the caller.
    55  func CheckPermission(ctx context.Context, perm realms.Permission, prefix types.StreamName, realm string) error {
    56  	// Log all the details to help debug permission issues.
    57  	ctx = logging.SetFields(ctx, logging.Fields{
    58  		"identity": auth.CurrentIdentity(ctx),
    59  		"perm":     perm,
    60  		"prefix":   prefix,
    61  		"realm":    realm,
    62  	})
    63  
    64  	// Check no cross-project mix up is happening as a precaution.
    65  	project := Project(ctx)
    66  	if projInRealm, _ := realms.Split(realm); projInRealm != project {
    67  		logging.Errorf(ctx, "Unexpectedly checking realm %q in a context of project %q", realm, project)
    68  		return status.Error(codes.Internal, "internal server error")
    69  	}
    70  
    71  	// Do the realms ACL check.
    72  	switch granted, err := auth.HasPermission(ctx, perm, realm, nil); {
    73  	case err != nil:
    74  		logging.WithError(err).Errorf(ctx, "failed to check realms ACL")
    75  		return status.Error(codes.Internal, "internal server error")
    76  	case granted:
    77  		logging.Debugf(ctx, "Permission granted")
    78  		return nil
    79  	default:
    80  		logging.Warningf(ctx, "Permission denied")
    81  		return PermissionDeniedErr(ctx)
    82  	}
    83  }
    84  
    85  // CheckAdminUser tests whether the current user belongs to the administrative
    86  // users group.
    87  //
    88  // Logs the outcome inside. The error is non-nil only if the check itself fails.
    89  func CheckAdminUser(ctx context.Context) (bool, error) {
    90  	cfg, err := config.Config(ctx)
    91  	if err != nil {
    92  		logging.WithError(err).Errorf(ctx, "Failed to load service config")
    93  		return false, err
    94  	}
    95  	return checkMember(ctx, "ADMIN", cfg.Coordinator.AdminAuthGroup)
    96  }
    97  
    98  // CheckServiceUser tests whether the current user belongs to the backend
    99  // services users group.
   100  //
   101  // Logs the outcome inside. The error is non-nil only if the check itself fails.
   102  func CheckServiceUser(ctx context.Context) (bool, error) {
   103  	cfg, err := config.Config(ctx)
   104  	if err != nil {
   105  		logging.WithError(err).Errorf(ctx, "Failed to load service config")
   106  		return false, err
   107  	}
   108  	return checkMember(ctx, "SERVICE", cfg.Coordinator.ServiceAuthGroup)
   109  }
   110  
   111  func checkMember(ctx context.Context, action string, groups ...string) (bool, error) {
   112  	switch yes, err := auth.IsMember(ctx, groups...); {
   113  	case err != nil:
   114  		logging.Fields{
   115  			"identity":       auth.CurrentIdentity(ctx),
   116  			"groups":         groups,
   117  			logging.ErrorKey: err,
   118  		}.Errorf(ctx, "Membership check failed")
   119  		return false, err
   120  	case yes:
   121  		logging.Fields{
   122  			"identity": auth.CurrentIdentity(ctx),
   123  			"groups":   groups,
   124  		}.Debugf(ctx, "User %s access granted.", action)
   125  		return true, nil
   126  	default:
   127  		logging.Fields{
   128  			"identity": auth.CurrentIdentity(ctx),
   129  			"groups":   groups,
   130  		}.Warningf(ctx, "User %s access denied.", action)
   131  		return false, nil
   132  	}
   133  }