go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/resultdb/internal/permissions/permissions.go (about)

     1  // Copyright 2020 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 permissions
    16  
    17  import (
    18  	"context"
    19  	"errors"
    20  	"fmt"
    21  
    22  	"google.golang.org/grpc/codes"
    23  
    24  	"go.chromium.org/luci/common/data/stringset"
    25  	"go.chromium.org/luci/grpc/appstatus"
    26  	"go.chromium.org/luci/resultdb/internal/invocations"
    27  	"go.chromium.org/luci/resultdb/internal/tracing"
    28  	"go.chromium.org/luci/server/auth"
    29  	"go.chromium.org/luci/server/auth/realms"
    30  )
    31  
    32  // VerifyInvocation checks if the caller has the specified permissions on the
    33  // realm that the invocation with the specified id belongs to.
    34  // There must must be a valid Spanner transaction in the given context, which
    35  // may be a span.Single().
    36  func VerifyInvocation(ctx context.Context, id invocations.ID, permissions ...realms.Permission) error {
    37  	return VerifyInvocations(ctx, invocations.NewIDSet(id), permissions...)
    38  }
    39  
    40  // VerifyInvocations checks multiple invocations' realms for the specified
    41  // permissions.
    42  // There must must be a valid Spanner transaction in the given context, which
    43  // may be a span.Single().
    44  func VerifyInvocations(ctx context.Context, ids invocations.IDSet, permissions ...realms.Permission) (err error) {
    45  	if len(ids) == 0 {
    46  		return nil
    47  	}
    48  	ctx, ts := tracing.Start(ctx, "resultdb.permissions.VerifyInvocations")
    49  	defer func() { tracing.End(ts, err) }()
    50  
    51  	realms, err := invocations.ReadRealms(ctx, ids)
    52  	if err != nil {
    53  		return err
    54  	}
    55  
    56  	// Note: HasPermissionsInRealms does not make RPCs.
    57  	verified, desc, err := HasPermissionsInRealms(ctx, realms, permissions...)
    58  	if err != nil {
    59  		return err
    60  	}
    61  	if !verified {
    62  		return appstatus.Errorf(codes.PermissionDenied, desc)
    63  	}
    64  
    65  	return nil
    66  }
    67  
    68  // VerifyInvocationsByName does the same as VerifyInvocations but accepts
    69  // invocation names instead of an invocations.IDSet.
    70  // There must must be a valid Spanner transaction in the given context, which
    71  // may be a span.Single().
    72  func VerifyInvocationsByName(ctx context.Context, invNames []string, permissions ...realms.Permission) error {
    73  	ids, err := invocations.ParseNames(invNames)
    74  	if err != nil {
    75  		return appstatus.BadRequest(err)
    76  	}
    77  	return VerifyInvocations(ctx, ids, permissions...)
    78  }
    79  
    80  // VerifyInvocationByName does the same as VerifyInvocation but accepts
    81  // an invocation name instead of an invocations.ID.
    82  // There must must be a valid Spanner transaction in the given context, which
    83  // may be a span.Single().
    84  func VerifyInvocationByName(ctx context.Context, invName string, permissions ...realms.Permission) error {
    85  	return VerifyInvocationsByName(ctx, []string{invName}, permissions...)
    86  }
    87  
    88  // HasPermissionsInRealms checks multiple invocations' realms for the specified
    89  // permissions. Returns:
    90  //   - whether the caller has all permissions in all invocations' realms
    91  //   - description of the first identified missing permission for an invocation
    92  //     (if applicable)
    93  //   - an error if one occurred
    94  func HasPermissionsInRealms(ctx context.Context, realms map[invocations.ID]string, permissions ...realms.Permission) (bool, string, error) {
    95  	checked := stringset.New(1)
    96  	for id, realm := range realms {
    97  		if !checked.Add(realm) {
    98  			continue
    99  		}
   100  		// Note: HasPermission does not make RPCs.
   101  		for _, permission := range permissions {
   102  			switch allowed, err := auth.HasPermission(ctx, permission, realm, nil); {
   103  			case err != nil:
   104  				return false, "", err
   105  			case !allowed:
   106  				return false, fmt.Sprintf(`caller does not have permission %s in realm of invocation %s`, permission, id), nil
   107  			}
   108  		}
   109  	}
   110  	return true, "", nil
   111  }
   112  
   113  // QuerySubRealmsNonEmpty returns subRealms that the user has the given permission in the given project.
   114  // It returns an appstatus annotated error if there is no realm in which the user has the permission.
   115  func QuerySubRealmsNonEmpty(ctx context.Context, project string, attrs realms.Attrs, permission realms.Permission) ([]string, error) {
   116  	if project == "" {
   117  		return nil, errors.New("project must be specified")
   118  	}
   119  	allowedRealms, err := auth.QueryRealms(ctx, permission, project, attrs)
   120  	if err != nil {
   121  		return nil, err
   122  	}
   123  	if len(allowedRealms) == 0 {
   124  		return nil, appstatus.Errorf(codes.PermissionDenied, `caller does not have permission %v in any realm in project %q`, permission, project)
   125  	}
   126  	subRealms := make([]string, 0, len(allowedRealms))
   127  	for _, r := range allowedRealms {
   128  		_, subRealm := realms.Split(r)
   129  		subRealms = append(subRealms, subRealm)
   130  	}
   131  	return subRealms, nil
   132  }