github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/service/git.go (about)

     1  // Copyright 2017 Keybase, Inc. All rights reserved. Use of
     2  // this source code is governed by the included BSD license.
     3  
     4  package service
     5  
     6  import (
     7  	"errors"
     8  	"fmt"
     9  	"time"
    10  
    11  	"github.com/keybase/client/go/git"
    12  	"github.com/keybase/client/go/libkb"
    13  	keybase1 "github.com/keybase/client/go/protocol/keybase1"
    14  	"github.com/keybase/client/go/teams"
    15  	"github.com/keybase/go-framed-msgpack-rpc/rpc"
    16  	"golang.org/x/net/context"
    17  )
    18  
    19  type GitHandler struct {
    20  	*BaseHandler
    21  	libkb.Contextified
    22  }
    23  
    24  var _ keybase1.GitInterface = (*GitHandler)(nil)
    25  
    26  const (
    27  	gitDefaultMaxLooseRefs         = 50
    28  	gitDefaultPruneMinLooseObjects = 50
    29  	gitDefaultPruneExpireAge       = 14 * 24 * time.Hour
    30  	gitDefaultMaxObjectPacks       = 50
    31  )
    32  
    33  func NewGitHandler(xp rpc.Transporter, g *libkb.GlobalContext) *GitHandler {
    34  	return &GitHandler{
    35  		BaseHandler:  NewBaseHandler(g, xp),
    36  		Contextified: libkb.NewContextified(g),
    37  	}
    38  }
    39  
    40  func (h *GitHandler) PutGitMetadata(ctx context.Context, arg keybase1.PutGitMetadataArg) (err error) {
    41  	ctx = libkb.WithLogTag(ctx, "GIT")
    42  	defer h.G().CTrace(ctx, fmt.Sprintf(
    43  		"git:PutGitMetadata(%v, %v, %v)", arg.RepoID, arg.Folder.Name, arg.Folder.FolderType),
    44  		&err)()
    45  
    46  	return git.PutMetadata(ctx, h.G(), arg)
    47  }
    48  
    49  func (h *GitHandler) DeleteGitMetadata(ctx context.Context, arg keybase1.DeleteGitMetadataArg) (err error) {
    50  	ctx = libkb.WithLogTag(ctx, "GIT")
    51  	defer h.G().CTrace(ctx, fmt.Sprintf(
    52  		"git:DeleteGitMetadata(%v, %v)", arg.Folder.Name, arg.Folder.FolderType),
    53  		&err)()
    54  
    55  	return git.DeleteMetadata(ctx, h.G(), arg.Folder, arg.RepoName)
    56  }
    57  
    58  func (h *GitHandler) GetGitMetadata(ctx context.Context, folder keybase1.FolderHandle) (res []keybase1.GitRepoResult, err error) {
    59  	ctx = libkb.WithLogTag(ctx, "GIT")
    60  	defer h.G().CTrace(ctx, fmt.Sprintf(
    61  		"git:GetGitMetadata(%v, %v)", folder.Name, folder.FolderType),
    62  		&err)()
    63  
    64  	return git.GetMetadata(ctx, h.G(), folder)
    65  }
    66  
    67  func (h *GitHandler) GetAllGitMetadata(ctx context.Context) (res []keybase1.GitRepoResult, err error) {
    68  	ctx = libkb.WithLogTag(ctx, "GIT")
    69  	defer h.G().CTrace(ctx, "git:GetAllGitMetadata", &err)()
    70  
    71  	return git.GetAllMetadata(ctx, h.G())
    72  }
    73  
    74  // In several cases (implicit admins doing anything, writers doing deletes),
    75  // KBFS will allow or give confusing error messages for operations that don't
    76  // have the right permissions. Doing an explicit check for these helps us give
    77  // clear errors.
    78  //
    79  // Note that the minimumRole here does *not* respect implicit adminship.
    80  func isRoleAtLeast(ctx context.Context, g *libkb.GlobalContext, teamName string, public bool, minimumRole keybase1.TeamRole) (bool, error) {
    81  	team, err := teams.Load(ctx, g, keybase1.LoadTeamArg{
    82  		Name:        teamName,
    83  		Public:      public,
    84  		ForceRepoll: true,
    85  	})
    86  	if err != nil {
    87  		return false, err
    88  	}
    89  	self, _, err := g.GetUPAKLoader().LoadV2(libkb.NewLoadUserSelfAndUIDArg(g))
    90  	if err != nil {
    91  		return false, err
    92  	}
    93  	role, err := team.MemberRole(ctx, self.Current.ToUserVersion())
    94  	if err != nil {
    95  		return false, fmt.Errorf("self role missing from team %s", teamName)
    96  	}
    97  	return role.IsOrAbove(minimumRole), nil
    98  }
    99  
   100  func (h *GitHandler) createRepo(ctx context.Context, folder keybase1.FolderHandle, repoName keybase1.GitRepoName, notifyTeam bool) (repoID keybase1.RepoID, err error) {
   101  	ctx = libkb.WithLogTag(ctx, "GIT")
   102  	defer h.G().CTrace(ctx, fmt.Sprintf(
   103  		"git:createRepo(%v, %v)", folder.Name, folder.FolderType),
   104  		&err)()
   105  
   106  	client, err := h.kbfsClient()
   107  	if err != nil {
   108  		return "", err
   109  	}
   110  
   111  	carg := keybase1.CreateRepoArg{
   112  		Folder: folder,
   113  		Name:   repoName,
   114  	}
   115  	repoID, err = client.CreateRepo(ctx, carg)
   116  	if err != nil {
   117  		// Real user errors are going to come through this path, like "repo
   118  		// already exists". Make them clear for the user.
   119  		return "", git.HumanizeGitErrors(ctx, h.G(), err)
   120  	}
   121  
   122  	// Currently KBFS will also call back into the service to put metadata
   123  	// after a create, so the put might happen twice, but we don't want to
   124  	// depend on that behavior.
   125  	err = git.PutMetadata(ctx, h.G(), keybase1.PutGitMetadataArg{
   126  		Folder: folder,
   127  		RepoID: repoID,
   128  		Metadata: keybase1.GitLocalMetadata{
   129  			RepoName: repoName,
   130  		},
   131  		NotifyTeam: notifyTeam,
   132  	})
   133  	if err != nil {
   134  		return "", err
   135  	}
   136  
   137  	return repoID, nil
   138  }
   139  
   140  func (h *GitHandler) CreatePersonalRepo(ctx context.Context, repoName keybase1.GitRepoName) (repoID keybase1.RepoID, err error) {
   141  	ctx = libkb.WithLogTag(ctx, "GIT")
   142  	defer h.G().CTrace(ctx, "git:CreatePersonalRepo", &err)()
   143  
   144  	folder := keybase1.FolderHandle{
   145  		Name:       h.G().Env.GetUsername().String(),
   146  		FolderType: keybase1.FolderType_PRIVATE,
   147  	}
   148  	return h.createRepo(ctx, folder, repoName, false /* notifyTeam */)
   149  }
   150  
   151  func (h *GitHandler) CreateTeamRepo(ctx context.Context, arg keybase1.CreateTeamRepoArg) (repoID keybase1.RepoID, err error) {
   152  	ctx = libkb.WithLogTag(ctx, "GIT")
   153  	defer h.G().CTrace(ctx, fmt.Sprintf(
   154  		"git:CreateTeamRepo(%v)", arg.TeamName),
   155  		&err)()
   156  
   157  	// Only support private teams
   158  	public := false
   159  
   160  	// This prevents implicit admins from getting a confusing error message.
   161  	isWriter, err := isRoleAtLeast(ctx, h.G(), arg.TeamName.String(), public, keybase1.TeamRole_WRITER)
   162  	if err != nil {
   163  		return "", err
   164  	}
   165  	if !isWriter {
   166  		return "", fmt.Errorf("Only team writers may create git repos.")
   167  	}
   168  
   169  	folder := keybase1.FolderHandle{
   170  		Name:       arg.TeamName.String(),
   171  		FolderType: keybase1.FolderType_TEAM,
   172  	}
   173  	return h.createRepo(ctx, folder, arg.RepoName, arg.NotifyTeam)
   174  }
   175  
   176  func (h *GitHandler) DeletePersonalRepo(ctx context.Context, repoName keybase1.GitRepoName) (err error) {
   177  	ctx = libkb.WithLogTag(ctx, "GIT")
   178  	defer h.G().CTrace(ctx, "git:DeletePersonalRepo",
   179  		&err)()
   180  
   181  	client, err := h.kbfsClient()
   182  	if err != nil {
   183  		return err
   184  	}
   185  	folder := keybase1.FolderHandle{
   186  		Name:       h.G().Env.GetUsername().String(),
   187  		FolderType: keybase1.FolderType_PRIVATE,
   188  	}
   189  	darg := keybase1.DeleteRepoArg{
   190  		Folder: folder,
   191  		Name:   repoName,
   192  	}
   193  	err = client.DeleteRepo(ctx, darg)
   194  	if err != nil {
   195  		switch err.(type) {
   196  		case libkb.RepoDoesntExistError:
   197  			h.G().Log.Warning("Git repo doesn't exist. Deleting metadata anyway.")
   198  		default:
   199  			return err
   200  		}
   201  	}
   202  
   203  	// Delete the repo metadata from the Keybase server.
   204  	err = git.DeleteMetadata(ctx, h.G(), folder, repoName)
   205  	return git.HumanizeGitErrors(ctx, h.G(), err)
   206  }
   207  
   208  func (h *GitHandler) DeleteTeamRepo(ctx context.Context, arg keybase1.DeleteTeamRepoArg) (err error) {
   209  	ctx = libkb.WithLogTag(ctx, "GIT")
   210  	defer h.G().CTrace(ctx, fmt.Sprintf(
   211  		"git:DeleteTeamRepo(%v)", arg.TeamName),
   212  		&err)()
   213  
   214  	// Only support private teams
   215  	public := false
   216  
   217  	// First make sure the user is an admin of the team. KBFS doesn't directly
   218  	// enforce this requirement, so a non-admin could get around it by hacking
   219  	// up their own client, but they could already wreak a lot of abuse by
   220  	// pushing garbage to the repo, so we don't consider this a big deal.
   221  	isAdmin, err := isRoleAtLeast(ctx, h.G(), arg.TeamName.String(), public, keybase1.TeamRole_ADMIN)
   222  	if err != nil {
   223  		return err
   224  	}
   225  	if !isAdmin {
   226  		return fmt.Errorf("Only team admins may delete git repos.")
   227  	}
   228  
   229  	client, err := h.kbfsClient()
   230  	if err != nil {
   231  		return err
   232  	}
   233  	folder := keybase1.FolderHandle{
   234  		Name:       arg.TeamName.String(),
   235  		FolderType: keybase1.FolderType_TEAM,
   236  	}
   237  	darg := keybase1.DeleteRepoArg{
   238  		Folder: folder,
   239  		Name:   arg.RepoName,
   240  	}
   241  	err = client.DeleteRepo(ctx, darg)
   242  	if err != nil {
   243  		switch err.(type) {
   244  		case libkb.RepoDoesntExistError:
   245  			h.G().Log.Warning("Git repo doesn't exist. Deleting metadata anyway.")
   246  		default:
   247  			return err
   248  		}
   249  	}
   250  
   251  	// Delete the repo metadata from the Keybase server.
   252  	err = git.DeleteMetadata(ctx, h.G(), folder, arg.RepoName)
   253  	return git.HumanizeGitErrors(ctx, h.G(), err)
   254  }
   255  
   256  func (h *GitHandler) GcPersonalRepo(ctx context.Context, arg keybase1.GcPersonalRepoArg) (err error) {
   257  	ctx = libkb.WithLogTag(ctx, "GIT")
   258  	defer h.G().CTrace(ctx, "git:GCPersonalRepo",
   259  		&err)()
   260  
   261  	client, err := h.kbfsClient()
   262  	if err != nil {
   263  		return err
   264  	}
   265  	folder := keybase1.FolderHandle{
   266  		Name:       h.G().Env.GetUsername().String(),
   267  		FolderType: keybase1.FolderType_PRIVATE,
   268  	}
   269  	options := keybase1.GcOptions{}
   270  	if !arg.Force {
   271  		options.MaxLooseRefs = gitDefaultMaxLooseRefs
   272  		options.MaxObjectPacks = gitDefaultMaxObjectPacks
   273  	}
   274  	gcarg := keybase1.GcArg{
   275  		Folder:  folder,
   276  		Name:    arg.RepoName,
   277  		Options: options,
   278  	}
   279  	err = client.Gc(ctx, gcarg)
   280  	if err != nil {
   281  		return git.HumanizeGitErrors(ctx, h.G(), err)
   282  	}
   283  	return nil
   284  }
   285  
   286  func (h *GitHandler) GcTeamRepo(ctx context.Context, arg keybase1.GcTeamRepoArg) (err error) {
   287  	ctx = libkb.WithLogTag(ctx, "GIT")
   288  	defer h.G().CTrace(ctx, fmt.Sprintf(
   289  		"git:GcTeamRepo(%v)", arg.TeamName),
   290  		&err)()
   291  
   292  	// Only support private teams
   293  	public := false
   294  
   295  	// First make sure the user is a writer of the team.
   296  	isWriter, err := isRoleAtLeast(ctx, h.G(), arg.TeamName.String(), public, keybase1.TeamRole_WRITER)
   297  	if err != nil {
   298  		return err
   299  	}
   300  	if !isWriter {
   301  		return fmt.Errorf("Only writers may garbage collect git repos.")
   302  	}
   303  
   304  	client, err := h.kbfsClient()
   305  	if err != nil {
   306  		return err
   307  	}
   308  	folder := keybase1.FolderHandle{
   309  		Name:       arg.TeamName.String(),
   310  		FolderType: keybase1.FolderType_TEAM,
   311  	}
   312  	options := keybase1.GcOptions{
   313  		PruneExpireTime: keybase1.ToTime(
   314  			time.Now().Add(-gitDefaultPruneExpireAge)),
   315  	}
   316  	if !arg.Force {
   317  		options.MaxLooseRefs = gitDefaultMaxLooseRefs
   318  		options.PruneMinLooseObjects = gitDefaultPruneMinLooseObjects
   319  	}
   320  	gcarg := keybase1.GcArg{
   321  		Folder:  folder,
   322  		Name:    arg.RepoName,
   323  		Options: options,
   324  	}
   325  	err = client.Gc(ctx, gcarg)
   326  	if err != nil {
   327  		return git.HumanizeGitErrors(ctx, h.G(), err)
   328  	}
   329  	return nil
   330  }
   331  
   332  func (h *GitHandler) GetTeamRepoSettings(ctx context.Context, arg keybase1.GetTeamRepoSettingsArg) (keybase1.GitTeamRepoSettings, error) {
   333  	return git.GetTeamRepoSettings(ctx, h.G(), arg)
   334  }
   335  
   336  func (h *GitHandler) SetTeamRepoSettings(ctx context.Context, arg keybase1.SetTeamRepoSettingsArg) error {
   337  	return git.SetTeamRepoSettings(ctx, h.G(), arg)
   338  }
   339  
   340  func (h *GitHandler) kbfsClient() (*keybase1.KBFSGitClient, error) {
   341  	if !h.G().ActiveDevice.Valid() {
   342  		return nil, libkb.LoginRequiredError{}
   343  	}
   344  	if h.G().ConnectionManager == nil {
   345  		return nil, errors.New("no connection manager available")
   346  	}
   347  	xp := h.G().ConnectionManager.LookupByClientType(keybase1.ClientType_KBFS)
   348  	if xp == nil {
   349  		return nil, libkb.KBFSNotRunningError{}
   350  	}
   351  	return &keybase1.KBFSGitClient{
   352  		Cli: rpc.NewClient(
   353  			xp, libkb.NewContextifiedErrorUnwrapper(h.G()), libkb.LogTagsFromContext),
   354  	}, nil
   355  }