internal/sshd/lfs.go

e2dec5d9ff2cd5dd54f68adec4190d8bafeaf302
gitbay/internal/sshd/lfs.go history · blame · raw

78 lines · 2559 bytes

 1package sshd
 2
 3import (
 4	"encoding/json"
 5	"fmt"
 6	"io"
 7	"time"
 8
 9	"gitbay.org/gitbay/internal/config"
10	"gitbay.org/gitbay/internal/lfs"
11	"gitbay.org/gitbay/internal/policy"
12	"gitbay.org/gitbay/internal/protocol"
13	"gitbay.org/gitbay/internal/store"
14)
15
16// runLFSAuthenticate answers the git-lfs client's SSH probe:
17//
18//	git-lfs-authenticate <path> download|upload
19//
20// with the HTTP endpoint and a short-lived repo- and operation-scoped
21// token. Access rules mirror the git transports: download needs read,
22// upload needs write; deploy keys authorize by their binding alone, and
23// every denial on an invisible repo reads as nonexistence.
24func runLFSAuthenticate(cfg config.Config, st *store.Store, user store.User, scope string,
25	argv []string, stdout, stderr io.Writer) int {
26	if len(argv) != 3 || (argv[2] != "download" && argv[2] != "upload") {
27		fmt.Fprintln(stderr, "usage: git-lfs-authenticate <path> download|upload")
28		return protocol.ExitUsage
29	}
30	op := argv[2]
31	write := op == "upload"
32	repo, err := st.RepoByPath(argv[1])
33	if err != nil {
34		fmt.Fprintln(stderr, "repository not found")
35		return protocol.ExitNotFound
36	}
37	if policy.IsDeployScope(scope) {
38		if !policy.DeployScopeAllows(scope, repo.ID, write) {
39			fmt.Fprintln(stderr, "repository not found")
40			return protocol.ExitNotFound
41		}
42	} else {
43		grant, err := st.AccessRole(repo.ID, user.ID)
44		if err != nil {
45			fmt.Fprintln(stderr, "internal error")
46			return protocol.ExitFailure
47		}
48		if !policy.CanRead(user, repo, grant) {
49			fmt.Fprintln(stderr, "repository not found")
50			return protocol.ExitNotFound
51		}
52		if !policy.ScopeAllowsGit(scope, repo.Path(), write) {
53			fmt.Fprintf(stderr, "this key's scope (%s) does not allow lfs %s on %s\n", scope, op, repo.Path())
54			return protocol.ExitDenied
55		}
56		if write && !policy.CanWrite(user, repo, grant) {
57			fmt.Fprintf(stderr, "write access to %s denied\n", repo.Path())
58			return protocol.ExitDenied
59		}
60	}
61	if write && repo.Settings.Archived {
62		fmt.Fprintf(stderr, "%s is archived and read-only\n", repo.Path())
63		return protocol.ExitDenied
64	}
65	secret, err := st.LFSSecret(lfs.NewSecret)
66	if err != nil {
67		fmt.Fprintln(stderr, "internal error")
68		return protocol.ExitFailure
69	}
70	token := lfs.Sign([]byte(secret), repo.ID, op, time.Now())
71	json.NewEncoder(stdout).Encode(map[string]any{
72		"href": fmt.Sprintf("%s/%s/%s.git/info/lfs",
73			cfg.Server.SiteURL, repo.OwnerName, repo.Name),
74		"header":     map[string]string{"Authorization": "Bearer " + token},
75		"expires_in": int(lfs.TokenTTL.Seconds()),
76	})
77	return protocol.ExitOK
78}