go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/teams/rpc/status.go (about) 1 // Copyright 2024 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 rpc contains the RPC handlers for the tree status service. 16 package rpc 17 18 import ( 19 "context" 20 "fmt" 21 "regexp" 22 23 "google.golang.org/grpc/codes" 24 "google.golang.org/protobuf/types/known/timestamppb" 25 26 "go.chromium.org/luci/common/errors" 27 "go.chromium.org/luci/grpc/appstatus" 28 "go.chromium.org/luci/server/span" 29 30 "go.chromium.org/luci/teams/internal/teams" 31 pb "go.chromium.org/luci/teams/proto/v1" 32 ) 33 34 type teamsServer struct{} 35 36 var _ pb.TeamsServer = &teamsServer{} 37 38 // NewTeamsServer creates a new server to handle Teams requests. 39 func NewTeamsServer() *pb.DecoratedTeams { 40 return &pb.DecoratedTeams{ 41 Prelude: checkAllowedPrelude, 42 Service: &teamsServer{}, 43 Postlude: gRPCifyAndLogPostlude, 44 } 45 } 46 47 // toTeamProto converts a teams.Team value to a pb.Team proto. 48 func toTeamProto(value *teams.Team) *pb.Team { 49 return &pb.Team{ 50 Name: fmt.Sprintf("teams/%s", value.ID), 51 CreateTime: timestamppb.New(value.CreateTime), 52 } 53 } 54 55 // Get gets a team. 56 // Use the resource alias 'my' to get just the current user's team. 57 func (*teamsServer) Get(ctx context.Context, request *pb.GetTeamRequest) (*pb.Team, error) { 58 id, err := parseTeamName(request.Name) 59 if err != nil { 60 return nil, invalidArgumentError(errors.Annotate(err, "name").Err()) 61 } 62 63 if id == "my" { 64 // TODO: Resolve team for user. 65 return nil, appstatus.Errorf(codes.Unimplemented, "resolving team for current user not yet implemented") 66 } 67 s, err := teams.Read(span.Single(ctx), id) 68 if errors.Is(err, teams.NotExistsErr) { 69 return nil, notFoundError(err) 70 } else if err != nil { 71 return nil, errors.Annotate(err, "reading team").Err() 72 } 73 74 return toTeamProto(s), nil 75 } 76 77 var teamNameRE = regexp.MustCompile(`^teams/(` + teams.TeamIDExpression + `|my)$`) 78 79 // parseTeamName parses a team resource name into its constituent ID 80 // parts. 81 func parseTeamName(name string) (id string, err error) { 82 if name == "" { 83 return "", errors.Reason("must be specified").Err() 84 } 85 match := teamNameRE.FindStringSubmatch(name) 86 if match == nil { 87 return "", errors.Reason("expected format: %s", teamNameRE).Err() 88 } 89 return match[1], nil 90 } 91 92 // invalidArgumentError annotates err as having an invalid argument. 93 // The error message is shared with the requester as is. 94 // 95 // Note that this differs from FailedPrecondition. It indicates arguments 96 // that are problematic regardless of the state of the system 97 // (e.g., a malformed file name). 98 func invalidArgumentError(err error) error { 99 return appstatus.Attachf(err, codes.InvalidArgument, "%s", err) 100 } 101 102 // notFoundError annotates err as being not found (HTTP 404). 103 // The error message is shared with the requester as is. 104 func notFoundError(err error) error { 105 return appstatus.Attachf(err, codes.NotFound, "%s", err) 106 }