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

     1  package git
     2  
     3  import (
     4  	"context"
     5  	"encoding/base64"
     6  	"fmt"
     7  	"sort"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/keybase/client/go/libkb"
    12  	"github.com/keybase/client/go/protocol/keybase1"
    13  	"github.com/keybase/client/go/teams"
    14  	"github.com/keybase/go-codec/codec"
    15  	"golang.org/x/sync/errgroup"
    16  )
    17  
    18  type ServerResponseRepo struct {
    19  	TeamID                keybase1.TeamID               `json:"team_id"`
    20  	RepoID                keybase1.RepoID               `json:"repo_id"`
    21  	CTime                 time.Time                     `json:"ctime"`
    22  	MTime                 time.Time                     `json:"mtime"`
    23  	EncryptedMetadata     string                        `json:"encrypted_metadata"`
    24  	EncryptionVersion     int                           `json:"encryption_version"`
    25  	Nonce                 string                        `json:"nonce"`
    26  	KeyGeneration         keybase1.PerTeamKeyGeneration `json:"key_generation"`
    27  	LastModifyingUID      keybase1.UID                  `json:"last_writer_uid"`
    28  	LastModifyingDeviceID keybase1.DeviceID             `json:"last_writer_device_id"`
    29  	ChatConvID            string                        `json:"chat_conv_id"`
    30  	ChatDisabled          bool                          `json:"chat_disabled"`
    31  	IsImplicit            bool                          `json:"is_implicit"`
    32  	Role                  keybase1.TeamRole             `json:"role"`
    33  }
    34  
    35  type ServerResponse struct {
    36  	Repos  []ServerResponseRepo `json:"repos"`
    37  	Status libkb.AppStatus      `json:"status"`
    38  }
    39  
    40  type ByRepoMtime []keybase1.GitRepoResult
    41  
    42  func (c ByRepoMtime) Len() int      { return len(c) }
    43  func (c ByRepoMtime) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
    44  func (c ByRepoMtime) Less(i, j int) bool {
    45  	res1, err1 := c[i].GetIfOk()
    46  	res2, err2 := c[j].GetIfOk()
    47  	if err1 != nil || err2 != nil {
    48  		return false
    49  	}
    50  
    51  	return res1.ServerMetadata.Mtime < res2.ServerMetadata.Mtime
    52  }
    53  
    54  var _ libkb.APIResponseWrapper = (*ServerResponse)(nil)
    55  
    56  // For GetDecode.
    57  func (r *ServerResponse) GetAppStatus() *libkb.AppStatus {
    58  	return &r.Status
    59  }
    60  
    61  func formatRepoURL(folder keybase1.FolderHandle, repoName string) string {
    62  	return "keybase://" + folder.ToString() + "/" + repoName
    63  }
    64  
    65  // The GUI needs a way to refer to repos
    66  func formatUniqueRepoID(teamID keybase1.TeamID, repoID keybase1.RepoID) string {
    67  	return string(teamID) + "_" + string(repoID)
    68  }
    69  
    70  // Implicit teams need to be converted back into the folder that matches their
    71  // display name. Regular teams become a regular team folder.
    72  func folderFromTeamID(ctx context.Context, g *libkb.GlobalContext, teamID keybase1.TeamID, isImplicit bool) (keybase1.FolderHandle, error) {
    73  	if isImplicit {
    74  		return folderFromTeamIDImplicit(ctx, g, teamID)
    75  	}
    76  	return folderFromTeamIDNamed(ctx, g, teamID)
    77  }
    78  
    79  func folderFromTeamIDNamed(ctx context.Context, g *libkb.GlobalContext, teamID keybase1.TeamID) (keybase1.FolderHandle, error) {
    80  	name, err := teams.ResolveIDToName(ctx, g, teamID)
    81  	if err != nil {
    82  		return keybase1.FolderHandle{}, err
    83  	}
    84  	return keybase1.FolderHandle{
    85  		Name:       name.String(),
    86  		FolderType: keybase1.FolderType_TEAM,
    87  	}, nil
    88  }
    89  
    90  // folderFromTeamIDImplicit converts from a teamID for implicit teams
    91  func folderFromTeamIDImplicit(ctx context.Context, g *libkb.GlobalContext, teamID keybase1.TeamID) (keybase1.FolderHandle, error) {
    92  
    93  	team, err := teams.Load(ctx, g, keybase1.LoadTeamArg{
    94  		ID:     teamID,
    95  		Public: teamID.IsPublic(),
    96  	})
    97  	if err != nil {
    98  		return keybase1.FolderHandle{}, err
    99  	}
   100  	if !team.IsImplicit() {
   101  		return keybase1.FolderHandle{}, fmt.Errorf("Expected an implicit team, but team load said otherwise (%s)", teamID)
   102  	}
   103  
   104  	// TODO: This function doesn't currently support conflict info.
   105  	name, err := team.ImplicitTeamDisplayNameString(ctx)
   106  	if err != nil {
   107  		return keybase1.FolderHandle{}, err
   108  	}
   109  	var folderType keybase1.FolderType
   110  	if team.IsPublic() {
   111  		folderType = keybase1.FolderType_PUBLIC
   112  	} else {
   113  		folderType = keybase1.FolderType_PRIVATE
   114  	}
   115  	return keybase1.FolderHandle{
   116  		Name:       name,
   117  		FolderType: folderType,
   118  	}, nil
   119  }
   120  
   121  // If folder is nil, get for all folders.
   122  func getMetadataInner(ctx context.Context, g *libkb.GlobalContext, folder *keybase1.FolderHandle) ([]keybase1.GitRepoResult, error) {
   123  	mctx := libkb.NewMetaContext(ctx, g)
   124  	teamer := NewTeamer(g)
   125  
   126  	apiArg := libkb.APIArg{
   127  		Endpoint:    "kbfs/git/team/get",
   128  		SessionType: libkb.APISessionTypeREQUIRED,
   129  		Args:        libkb.HTTPArgs{}, // a limit parameter exists, default 100, and we don't currently set it
   130  	}
   131  
   132  	// The team_id parameter is optional. Add it in if the caller supplied it.
   133  	if folder != nil {
   134  		teamIDVis, err := teamer.LookupOrCreate(ctx, *folder)
   135  		if err != nil {
   136  			return nil, err
   137  		}
   138  		apiArg.Args["team_id"] = libkb.S{Val: string(teamIDVis.TeamID)}
   139  	}
   140  	var serverResponse ServerResponse
   141  	err := mctx.G().GetAPI().GetDecode(mctx, apiArg, &serverResponse)
   142  	if err != nil {
   143  		return nil, err
   144  	}
   145  
   146  	// Unbox the repos in parallel
   147  	repoCh := make(chan ServerResponseRepo)
   148  	eg, ctx := errgroup.WithContext(ctx)
   149  	eg.Go(func() error {
   150  		defer close(repoCh)
   151  		for _, responseRepo := range serverResponse.Repos {
   152  			select {
   153  			case repoCh <- responseRepo:
   154  			case <-ctx.Done():
   155  				return ctx.Err()
   156  			}
   157  		}
   158  		return nil
   159  	})
   160  
   161  	// Initializing the results list to non-nil means that we end up seeing
   162  	// "[]" instead of "null" in the final JSON output on the CLI, which is
   163  	// preferable.
   164  	resultList := []keybase1.GitRepoResult{}
   165  	var resLock sync.Mutex
   166  	var firstErr error
   167  	var anySuccess bool
   168  	numUnboxThreads := 2
   169  	for i := 0; i < numUnboxThreads; i++ {
   170  		eg.Go(func() error {
   171  			for responseRepo := range repoCh {
   172  				info, skip, err := getMetadataInnerSingle(ctx, g, folder, responseRepo)
   173  
   174  				resLock.Lock()
   175  				if err != nil {
   176  					if firstErr == nil {
   177  						firstErr = err
   178  					}
   179  					mctx.Debug("git.getMetadataInner error (team:%v, repo:%v): %v", responseRepo.TeamID, responseRepo.RepoID, err)
   180  					resultList = append(resultList, keybase1.NewGitRepoResultWithErr(err.Error()))
   181  				} else if !skip {
   182  					anySuccess = true
   183  					resultList = append(resultList, keybase1.NewGitRepoResultWithOk(*info))
   184  				}
   185  				resLock.Unlock()
   186  
   187  			}
   188  			return nil
   189  		})
   190  	}
   191  	if err := eg.Wait(); err != nil {
   192  		return resultList, err
   193  	}
   194  	sort.Sort(ByRepoMtime(resultList))
   195  
   196  	// If there were no repos, return ok
   197  	// If all repos failed, return the first error (something is probably wrong that's not repo-specific)
   198  	// If no repos failed, return ok
   199  	if len(resultList) == 0 {
   200  		return resultList, nil
   201  	}
   202  	if !anySuccess {
   203  		return resultList, firstErr
   204  	}
   205  	return resultList, nil
   206  }
   207  
   208  // if skip is true the other return values are nil
   209  func getMetadataInnerSingle(ctx context.Context, g *libkb.GlobalContext,
   210  	folder *keybase1.FolderHandle, responseRepo ServerResponseRepo) (info *keybase1.GitRepoInfo, skip bool, err error) {
   211  
   212  	cryptoer := NewCrypto(g)
   213  
   214  	// If the folder was passed in, use it. Otherwise, load the team to
   215  	// figure it out.
   216  	var repoFolder keybase1.FolderHandle
   217  	if folder != nil {
   218  		repoFolder = *folder
   219  	} else {
   220  		repoFolder, err = folderFromTeamID(ctx, g, responseRepo.TeamID, responseRepo.IsImplicit)
   221  		if err != nil {
   222  			return nil, false, err
   223  		}
   224  
   225  		// Currently we want to pretend that multi-user personal repos
   226  		// (/keybase/{private,public}/chris,max/...) don't exist. Short circuit here
   227  		// to keep those out of the results list.
   228  		if repoFolder.Name != g.Env.GetUsername().String() &&
   229  			(repoFolder.FolderType == keybase1.FolderType_PRIVATE || repoFolder.FolderType == keybase1.FolderType_PUBLIC) {
   230  
   231  			return nil, true, nil
   232  		}
   233  	}
   234  
   235  	teamIDVis := keybase1.TeamIDWithVisibility{
   236  		TeamID: responseRepo.TeamID,
   237  	}
   238  	if repoFolder.FolderType != keybase1.FolderType_PUBLIC {
   239  		teamIDVis.Visibility = keybase1.TLFVisibility_PRIVATE
   240  	} else {
   241  		teamIDVis.Visibility = keybase1.TLFVisibility_PUBLIC
   242  	}
   243  
   244  	ciphertext, err := base64.StdEncoding.DecodeString(responseRepo.EncryptedMetadata)
   245  	if err != nil {
   246  		return nil, false, err
   247  	}
   248  
   249  	nonceSlice, err := base64.StdEncoding.DecodeString(responseRepo.Nonce)
   250  	if err != nil {
   251  		return nil, false, err
   252  	}
   253  	if len(nonceSlice) != len(keybase1.BoxNonce{}) {
   254  		return nil, false, fmt.Errorf("expected a nonce of length %d, found %d", len(keybase1.BoxNonce{}), len(nonceSlice))
   255  	}
   256  	var nonce keybase1.BoxNonce
   257  	copy(nonce[:], nonceSlice)
   258  
   259  	encryptedMetadata := keybase1.EncryptedGitMetadata{
   260  		V:   responseRepo.EncryptionVersion,
   261  		E:   ciphertext,
   262  		N:   nonce,
   263  		Gen: responseRepo.KeyGeneration,
   264  	}
   265  	msgpackPlaintext, err := cryptoer.Unbox(ctx, teamIDVis, &encryptedMetadata)
   266  	if err != nil {
   267  		return nil, false, fmt.Errorf("repo tid:%v visibility:%s: %v", teamIDVis.TeamID, teamIDVis.Visibility, err)
   268  	}
   269  
   270  	var localMetadataVersioned keybase1.GitLocalMetadataVersioned
   271  	mh := codec.MsgpackHandle{WriteExt: true}
   272  	dec := codec.NewDecoderBytes(msgpackPlaintext, &mh)
   273  	err = dec.Decode(&localMetadataVersioned)
   274  	if err != nil {
   275  		return nil, false, err
   276  	}
   277  
   278  	// Translate back from GitLocalMetadataVersioned to the decoupled type
   279  	// that we use for local RPC.
   280  	version, err := localMetadataVersioned.Version()
   281  	if err != nil {
   282  		return nil, false, err
   283  	}
   284  	var localMetadata keybase1.GitLocalMetadata
   285  	switch version {
   286  	case keybase1.GitLocalMetadataVersion_V1:
   287  		localMetadata = keybase1.GitLocalMetadata{
   288  			RepoName: localMetadataVersioned.V1().RepoName,
   289  		}
   290  	default:
   291  		return nil, false, fmt.Errorf("unrecognized variant of GitLocalMetadataVersioned: %#v", version)
   292  	}
   293  
   294  	// Load UPAKs to get the last writer username and device name.
   295  	lastWriterUPAK, _, err := g.GetUPAKLoader().LoadV2(libkb.NewLoadUserArgWithContext(ctx, g).
   296  		WithUID(responseRepo.LastModifyingUID).
   297  		WithPublicKeyOptional())
   298  	if err != nil {
   299  		return nil, false, err
   300  	}
   301  	var deviceName string
   302  	for _, upk := range append([]keybase1.UserPlusKeysV2{lastWriterUPAK.Current}, lastWriterUPAK.PastIncarnations...) {
   303  		for _, deviceKey := range upk.DeviceKeys {
   304  			if deviceKey.DeviceID.Eq(responseRepo.LastModifyingDeviceID) {
   305  				deviceName = deviceKey.DeviceDescription
   306  				break
   307  			}
   308  		}
   309  	}
   310  	if deviceName == "" {
   311  		return nil, false, fmt.Errorf("can't find device name for %s's device ID %s", lastWriterUPAK.Current.Username, responseRepo.LastModifyingDeviceID)
   312  	}
   313  
   314  	var settings *keybase1.GitTeamRepoSettings
   315  	if repoFolder.FolderType == keybase1.FolderType_TEAM {
   316  		pset, err := convertTeamRepoSettings(ctx, g, responseRepo.TeamID, responseRepo.ChatConvID, responseRepo.ChatDisabled)
   317  		if err != nil {
   318  			return nil, false, err
   319  		}
   320  		settings = &pset
   321  	}
   322  
   323  	return &keybase1.GitRepoInfo{
   324  		Folder:         repoFolder,
   325  		RepoID:         responseRepo.RepoID,
   326  		RepoUrl:        formatRepoURL(repoFolder, string(localMetadata.RepoName)),
   327  		GlobalUniqueID: formatUniqueRepoID(responseRepo.TeamID, responseRepo.RepoID),
   328  		CanDelete:      responseRepo.Role.IsAdminOrAbove(),
   329  		LocalMetadata:  localMetadata,
   330  		ServerMetadata: keybase1.GitServerMetadata{
   331  			Ctime:                   keybase1.ToTime(responseRepo.CTime),
   332  			Mtime:                   keybase1.ToTime(responseRepo.MTime),
   333  			LastModifyingUsername:   lastWriterUPAK.Current.Username,
   334  			LastModifyingDeviceID:   responseRepo.LastModifyingDeviceID,
   335  			LastModifyingDeviceName: deviceName,
   336  		},
   337  		TeamRepoSettings: settings,
   338  	}, false, nil
   339  }
   340  
   341  func GetMetadata(ctx context.Context, g *libkb.GlobalContext, folder keybase1.FolderHandle) ([]keybase1.GitRepoResult, error) {
   342  	return getMetadataInner(ctx, g, &folder)
   343  }
   344  
   345  func GetAllMetadata(ctx context.Context, g *libkb.GlobalContext) ([]keybase1.GitRepoResult, error) {
   346  	return getMetadataInner(ctx, g, nil)
   347  }