go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cipd/appengine/ui/metadata.go (about) 1 // Copyright 2018 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 ui 16 17 import ( 18 "context" 19 "sort" 20 "strings" 21 22 "google.golang.org/grpc/codes" 23 "google.golang.org/grpc/status" 24 25 "go.chromium.org/luci/auth/identity" 26 "go.chromium.org/luci/common/data/stringset" 27 "go.chromium.org/luci/server/auth" 28 "go.chromium.org/luci/server/auth/authdb" 29 30 api "go.chromium.org/luci/cipd/api/cipd/v1" 31 ) 32 33 // prefixMetadataBlock is passed to the templates as Metadata arg. 34 type prefixMetadataBlock struct { 35 // CanView is true if the caller is able to see all prefix metadata. 36 CanView bool 37 38 // ACLs is per-principal acls sorted by prefix length and role. 39 // 40 // Populated only if CanView is true. 41 ACLs []*metadataACL 42 43 // CallerRoles is roles of the current caller. 44 // 45 // Populated only if CanView is false. 46 CallerRoles []string 47 } 48 49 type metadataACL struct { 50 RolePb api.Role // original role enum, for sorting 51 Role string // e.g. "Reader" 52 Who string // either an email or a group name 53 WhoHref string // for groups, link to a group definition 54 Group bool // if true, this entry is a group 55 Missing bool // if true, this entry refers to a missing group 56 Prefix string // via what prefix this role is granted 57 PrefixHref string // link to the corresponding prefix page 58 } 59 60 // fetchPrefixMetadata fetches and formats for UI metadata of the given prefix. 61 // 62 // It recognizes PermissionDenied errors and falls back to only displaying what 63 // roles the caller has instead of the full metadata. 64 func fetchPrefixMetadata(ctx context.Context, pfx string) (*prefixMetadataBlock, error) { 65 meta, err := state(ctx).services.PublicRepo.GetInheritedPrefixMetadata(ctx, &api.PrefixRequest{ 66 Prefix: pfx, 67 }) 68 switch status.Code(err) { 69 case codes.OK: 70 break // handled below 71 case codes.PermissionDenied: 72 return fetchCallerRoles(ctx, pfx) 73 default: 74 return nil, err 75 } 76 77 db := auth.GetState(ctx).DB() 78 79 // Grab URL of an auth server with the groups, if available. 80 groupsURL := "" 81 if url, err := db.GetAuthServiceURL(ctx); err == nil { 82 groupsURL = url + "/auth/groups/" 83 } 84 85 // Collect all groups mentioned by the ACL to flag unknown ones. 86 groups := stringset.New(0) 87 88 out := &prefixMetadataBlock{CanView: true} 89 for _, m := range meta.PerPrefixMetadata { 90 for _, a := range m.Acls { 91 role := strings.Title(strings.ToLower(a.Role.String())) 92 93 prefix := m.Prefix 94 if prefix == "" { 95 prefix = "[root]" 96 } 97 98 for _, p := range a.Principals { 99 whoHref := "" 100 group := false 101 switch { 102 case strings.HasPrefix(p, "group:"): 103 p = strings.TrimPrefix(p, "group:") 104 if groupsURL != "" { 105 whoHref = groupsURL + p 106 } 107 groups.Add(p) 108 group = true 109 case p == string(identity.AnonymousIdentity): 110 p = "anonymous" 111 default: 112 p = strings.TrimPrefix(p, "user:") 113 } 114 115 out.ACLs = append(out.ACLs, &metadataACL{ 116 RolePb: a.Role, 117 Role: role, 118 Who: p, 119 WhoHref: whoHref, 120 Group: group, 121 Prefix: prefix, 122 PrefixHref: listingPageURL(m.Prefix, ""), 123 }) 124 } 125 } 126 } 127 128 sort.Slice(out.ACLs, func(i, j int) bool { 129 l, r := out.ACLs[i], out.ACLs[j] 130 if l.RolePb != r.RolePb { 131 return l.RolePb > r.RolePb // "stronger" role (e.g. OWNERS) first 132 } 133 if l.Prefix != r.Prefix { 134 return l.Prefix > r.Prefix // longer prefix first 135 } 136 return l.Who < r.Who // alphabetically 137 }) 138 139 // If the caller is allowed to see actual contents of groups, flag groups that 140 // do not exist. 141 if groups.Len() > 0 { 142 switch yes, err := db.IsMember(ctx, auth.CurrentIdentity(ctx), []string{authdb.AuthServiceAccessGroup}); { 143 case yes: 144 known, err := db.FilterKnownGroups(ctx, groups.ToSlice()) 145 if err != nil { 146 return nil, err 147 } 148 knownSet := stringset.NewFromSlice(known...) 149 for _, acl := range out.ACLs { 150 acl.Missing = acl.Group && !knownSet.Has(acl.Who) 151 } 152 case err != nil: 153 return nil, err 154 } 155 } 156 157 return out, nil 158 } 159 160 func fetchCallerRoles(ctx context.Context, pfx string) (*prefixMetadataBlock, error) { 161 roles, err := state(ctx).services.PublicRepo.GetRolesInPrefix(ctx, &api.PrefixRequest{ 162 Prefix: pfx, 163 }) 164 if err != nil { 165 return nil, err 166 } 167 out := &prefixMetadataBlock{ 168 CanView: false, 169 CallerRoles: make([]string, len(roles.Roles)), 170 } 171 for i, r := range roles.Roles { 172 out.CallerRoles[i] = strings.Title(strings.ToLower(r.Role.String())) 173 } 174 return out, nil 175 }