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  }