go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/auth_service/impl/servers/groups/server.go (about) 1 // Copyright 2021 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 groups contains Groups server implementation. 16 package groups 17 18 import ( 19 "context" 20 "errors" 21 22 "google.golang.org/grpc/codes" 23 "google.golang.org/grpc/status" 24 "google.golang.org/protobuf/types/known/emptypb" 25 26 "go.chromium.org/luci/common/logging" 27 "go.chromium.org/luci/gae/service/datastore" 28 "go.chromium.org/luci/server/auth" 29 30 "go.chromium.org/luci/auth_service/api/rpcpb" 31 "go.chromium.org/luci/auth_service/impl/model" 32 "go.chromium.org/luci/auth_service/impl/model/graph" 33 ) 34 35 // Server implements Groups server. 36 type Server struct { 37 rpcpb.UnimplementedGroupsServer 38 39 // Whether modifications should be done in dry run mode (i.e. skip 40 // committing entity changes). 41 dryRun bool 42 } 43 44 func NewServer(dryRun bool) *Server { 45 return &Server{ 46 dryRun: dryRun, 47 } 48 } 49 50 // ListGroups implements the corresponding RPC method. 51 func (*Server) ListGroups(ctx context.Context, _ *emptypb.Empty) (*rpcpb.ListGroupsResponse, error) { 52 // Get groups from datastore. 53 groups, err := model.GetAllAuthGroups(ctx) 54 if err != nil { 55 return nil, status.Errorf(codes.Internal, "failed to fetch groups: %s", err) 56 } 57 58 var groupList = make([]*rpcpb.AuthGroup, len(groups)) 59 for idx, entity := range groups { 60 g := entity.ToProto(false) 61 g.CallerCanModify = canCallerModify(ctx, entity) 62 groupList[idx] = g 63 } 64 65 return &rpcpb.ListGroupsResponse{ 66 Groups: groupList, 67 }, nil 68 } 69 70 // GetGroup implements the corresponding RPC method. 71 func (*Server) GetGroup(ctx context.Context, request *rpcpb.GetGroupRequest) (*rpcpb.AuthGroup, error) { 72 switch group, err := model.GetAuthGroup(ctx, request.Name); { 73 case err == nil: 74 g := group.ToProto(true) 75 g.CallerCanModify = canCallerModify(ctx, group) 76 return g, nil 77 case errors.Is(err, datastore.ErrNoSuchEntity): 78 return nil, status.Errorf(codes.NotFound, "no such group %q", request.Name) 79 default: 80 return nil, status.Errorf(codes.Internal, "failed to fetch group %q: %s", request.Name, err) 81 } 82 } 83 84 // CreateGroup implements the corresponding RPC method. 85 func (srv *Server) CreateGroup(ctx context.Context, request *rpcpb.CreateGroupRequest) (*rpcpb.AuthGroup, error) { 86 group := model.AuthGroupFromProto(ctx, request.GetGroup()) 87 switch createdGroup, err := model.CreateAuthGroup(ctx, group, false, "Go pRPC API", srv.dryRun); { 88 case err == nil: 89 return createdGroup.ToProto(true), nil 90 case errors.Is(err, model.ErrAlreadyExists): 91 return nil, status.Errorf(codes.AlreadyExists, "group already exists: %s", err) 92 case errors.Is(err, model.ErrInvalidName): 93 return nil, status.Errorf(codes.InvalidArgument, "invalid group name: %s", err) 94 case errors.Is(err, model.ErrInvalidReference): 95 return nil, status.Errorf(codes.InvalidArgument, "invalid group reference: %s", err) 96 case errors.Is(err, model.ErrInvalidIdentity): 97 return nil, status.Errorf(codes.InvalidArgument, "%s", err) 98 default: 99 return nil, status.Errorf(codes.Internal, "failed to create group %q: %s", request.GetGroup().GetName(), err) 100 } 101 } 102 103 // UpdateGroup implements the corresponding RPC method. 104 func (srv *Server) UpdateGroup(ctx context.Context, request *rpcpb.UpdateGroupRequest) (*rpcpb.AuthGroup, error) { 105 groupUpdate := model.AuthGroupFromProto(ctx, request.GetGroup()) 106 switch updatedGroup, err := model.UpdateAuthGroup(ctx, groupUpdate, request.GetUpdateMask(), request.GetGroup().GetEtag(), false, "Go pRPC API", srv.dryRun); { 107 case err == nil: 108 return updatedGroup.ToProto(true), nil 109 case errors.Is(err, datastore.ErrNoSuchEntity): 110 return nil, status.Errorf(codes.NotFound, "no such group %q", groupUpdate.ID) 111 case errors.Is(err, model.ErrPermissionDenied): 112 return nil, status.Errorf(codes.PermissionDenied, "%s does not have permission to update group %q: %s", auth.CurrentIdentity(ctx), groupUpdate.ID, err) 113 case errors.Is(err, model.ErrConcurrentModification): 114 return nil, status.Error(codes.Aborted, err.Error()) 115 case errors.Is(err, model.ErrInvalidReference): 116 return nil, status.Errorf(codes.InvalidArgument, "invalid group reference: %s", err) 117 case errors.Is(err, model.ErrInvalidArgument): 118 return nil, status.Error(codes.InvalidArgument, err.Error()) 119 case errors.Is(err, model.ErrInvalidIdentity): 120 return nil, status.Error(codes.InvalidArgument, err.Error()) 121 case errors.Is(err, model.ErrCyclicDependency): 122 return nil, status.Error(codes.FailedPrecondition, err.Error()) 123 default: 124 return nil, status.Errorf(codes.Internal, "failed to update group %q: %s", request.GetGroup().GetName(), err) 125 } 126 } 127 128 // DeleteGroup implements the corresponding RPC method. 129 func (srv *Server) DeleteGroup(ctx context.Context, request *rpcpb.DeleteGroupRequest) (*emptypb.Empty, error) { 130 name := request.GetName() 131 switch err := model.DeleteAuthGroup(ctx, name, request.GetEtag(), false, "Go pRPC API", srv.dryRun); { 132 case err == nil: 133 return &emptypb.Empty{}, nil 134 case errors.Is(err, datastore.ErrNoSuchEntity): 135 return nil, status.Errorf(codes.NotFound, "no such group %q", name) 136 case errors.Is(err, model.ErrPermissionDenied): 137 return nil, status.Errorf(codes.PermissionDenied, "%s does not have permission to delete group %q: %s", auth.CurrentIdentity(ctx), name, err) 138 case errors.Is(err, model.ErrConcurrentModification): 139 return nil, status.Error(codes.Aborted, err.Error()) 140 case errors.Is(err, model.ErrReferencedEntity): 141 return nil, status.Error(codes.FailedPrecondition, err.Error()) 142 // TODO(cjacomet): Handle context cancelled and internal errors in a more general way with logging. 143 case errors.Is(err, context.Canceled): 144 return nil, status.Error(codes.Canceled, err.Error()) 145 default: 146 return nil, status.Errorf(codes.Internal, "failed to delete group %q: %s", name, err) 147 } 148 } 149 150 // GetSubgraph implements the corresponding RPC method. 151 // 152 // Possible Errors: 153 // 154 // Internal error for datastore access issues. 155 // NotFound error wrapping a graph.ErrNoSuchGroup if group is not present in groups graph. 156 // InvalidArgument error if the PrincipalKind is unspecified. 157 // Annotated error if the subgraph building fails, this may be an InvalidArgument or NotFound error. 158 func (*Server) GetSubgraph(ctx context.Context, request *rpcpb.GetSubgraphRequest) (*rpcpb.Subgraph, error) { 159 // Get groups from datastore. 160 groups, err := model.GetAllAuthGroups(ctx) 161 if err != nil { 162 return nil, status.Errorf(codes.Internal, "failed to fetch groups %s", err) 163 } 164 165 // Build groups graph from groups in datastore. 166 groupsGraph := graph.NewGraph(groups) 167 168 principal, err := convertPrincipal(request.Principal) 169 if err != nil { 170 return nil, err 171 } 172 173 subgraph, err := groupsGraph.GetRelevantSubgraph(principal) 174 if err != nil { 175 return nil, err 176 } 177 178 subgraphProto := subgraph.ToProto() 179 return subgraphProto, nil 180 } 181 182 // convertPrincipal handles the conversion of rpcpb.Principal -> graph.NodeKey 183 func convertPrincipal(p *rpcpb.Principal) (graph.NodeKey, error) { 184 switch p.Kind { 185 case rpcpb.PrincipalKind_GLOB: 186 return graph.NodeKey{Kind: graph.Glob, Value: p.Name}, nil 187 case rpcpb.PrincipalKind_IDENTITY: 188 return graph.NodeKey{Kind: graph.Identity, Value: p.Name}, nil 189 case rpcpb.PrincipalKind_GROUP: 190 return graph.NodeKey{Kind: graph.Group, Value: p.Name}, nil 191 default: 192 return graph.NodeKey{}, status.Errorf(codes.InvalidArgument, "invalid principal kind") 193 } 194 } 195 196 // canCallerModify returns whether the current identity can modify the 197 // given group. 198 func canCallerModify(ctx context.Context, g *model.AuthGroup) bool { 199 if model.IsExternalAuthGroupName(g.ID) { 200 return false 201 } 202 203 isOwner, err := auth.IsMember(ctx, model.AdminGroup, g.Owners) 204 if err != nil { 205 logging.Errorf(ctx, "error checking group owner status: %w", err) 206 return false 207 } 208 return isOwner 209 }