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 }