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 }