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  }