243 lines
7.2 KiB
Go
243 lines
7.2 KiB
Go
// Database engine opener for simplegit's private DB.
|
|
//
|
|
// Two backends, selected by URI scheme:
|
|
//
|
|
// sqlite://<path> -> modernc.org/sqlite (pure Go, no CGO; single binary)
|
|
// postgres://<dsn-url> -> github.com/lib/pq
|
|
//
|
|
// Both are driven through xorm (see models.go's InitEngine). modernc registers
|
|
// its driver under the name "sqlite" by default, but xorm keys its sqlite
|
|
// dialect off "sqlite3" -- so we re-register modernc's driver as "sqlite3" in
|
|
// init(). This is the same trick simpleci's driver_sqlite_modernc.go uses, and
|
|
// it keeps CGO out of the build (unlike mattn/go-sqlite3).
|
|
|
|
package state
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ecdsa"
|
|
"crypto/ed25519"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/sha256"
|
|
"crypto/x509"
|
|
"database/sql"
|
|
"encoding/hex"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"simplegit/common"
|
|
"simplegit/gitcmd"
|
|
"strings"
|
|
"sync"
|
|
|
|
_ "github.com/lib/pq" // postgres driver, registered as "postgres"
|
|
"golang.org/x/crypto/ssh"
|
|
"modernc.org/sqlite" // pure-Go sqlite driver; re-registered as "sqlite3" below
|
|
"xorm.io/xorm"
|
|
)
|
|
|
|
const sqliteDriver = "sqlite3"
|
|
|
|
type IState interface {
|
|
Open(owner, name string) (*gitcmd.Repository, error)
|
|
// should check database, if not exist, return err directly
|
|
RepoPath(owner, name string) (string, error)
|
|
KnownSSHKey(key ssh.PublicKey) (bool, error)
|
|
AccessBySSHKey(ctx context.Context, key ssh.PublicKey, repons, reponame string, perm common.Perm) (ok bool, err error)
|
|
AccessByPAT(ctx context.Context, pat, repons, reponame string, perm common.Perm) (ok bool, err error)
|
|
Signers() ([]ssh.Signer, error)
|
|
}
|
|
|
|
type IStateMut interface {
|
|
IState
|
|
MoveNS(old, new string) error
|
|
MoveRepo(old, new string) error
|
|
|
|
CreateRepo(ns, name string) error
|
|
ExistRepo(ns, name string) error
|
|
DeleteRepo(ns, name string) error
|
|
|
|
ACLUpsertSSHKeyOnNS(ns string, key ssh.PublicKey, perm common.Perm) (aclID int64, err error)
|
|
ACLUpsertPATOnNS(ns string, pat string, perm common.Perm) (aclID int64, err error)
|
|
ACLUpsertSSHKeyOnRepo(reponame string, key ssh.PublicKey, perm common.Perm) (aclID int64, err error)
|
|
ACLUpsertPATOnRepo(reponame string, pat string, perm common.Perm) (aclID int64, err error)
|
|
ACLDelete(id int64) error
|
|
}
|
|
|
|
// Compile-time guarantees that LocalState satisfies both views.
|
|
var (
|
|
_ IState = (*LocalState)(nil)
|
|
_ IStateMut = (*LocalState)(nil)
|
|
)
|
|
|
|
type LocalState struct {
|
|
engine *xorm.Engine
|
|
cfg *common.Config
|
|
|
|
signerMu sync.Mutex
|
|
signers []ssh.Signer
|
|
signerDone bool
|
|
}
|
|
|
|
func init() {
|
|
// Register modernc's driver under "sqlite3" so xorm picks up its sqlite
|
|
// dialect while the actual driver stays CGO-free.
|
|
sql.Register(sqliteDriver, &sqlite.Driver{})
|
|
}
|
|
|
|
func Open(cfg *common.Config) (*LocalState, error) {
|
|
engine, err := InitEngine(cfg.Driver(), cfg.DSN())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("state: init engine: %w", err)
|
|
}
|
|
if cfg.Driver() == sqliteDriver {
|
|
engine.SetMaxOpenConns(1)
|
|
}
|
|
log.Printf("state: RepoRoot: %s", cfg.RepoRoot())
|
|
log.Printf("state: DB engine %s opened, %s", cfg.Driver(), cfg.DSN())
|
|
return &LocalState{
|
|
engine: engine,
|
|
cfg: cfg,
|
|
}, nil
|
|
}
|
|
|
|
// Signer returns the SSH host key, creating and persisting it on first use.
|
|
// Cached so the key is stable for the process lifetime.
|
|
func (s *LocalState) Signers() ([]ssh.Signer, error) {
|
|
s.signerMu.Lock()
|
|
defer s.signerMu.Unlock()
|
|
if s.signerDone {
|
|
return s.signers, nil
|
|
}
|
|
s.signerDone = true
|
|
ed25519signer, err := s.loadOrCreateHostKey("ed25519")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rsasigner, err := s.loadOrCreateHostKey("rsa")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ecdsasigner, err := s.loadOrCreateHostKey("ecdsa")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
s.signers = append(s.signers, ed25519signer, rsasigner, ecdsasigner)
|
|
return s.signers, err
|
|
}
|
|
|
|
func (s *LocalState) loadOrCreateHostKey(ty string) (ssh.Signer, error) {
|
|
var der []byte
|
|
switch ty {
|
|
case "ed25519":
|
|
_, priv, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
der, err = x509.MarshalPKCS8PrivateKey(priv)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
case "rsa":
|
|
priv, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
der, err = x509.MarshalPKCS8PrivateKey(priv)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
case "ecdsa":
|
|
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
der, err = x509.MarshalPKCS8PrivateKey(priv)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
default:
|
|
return nil, errors.New("loadOrCreateHostKey")
|
|
}
|
|
hostKeyPath := filepath.Join(s.cfg.Root, fmt.Sprintf("host_key_%v.pem", ty))
|
|
if data, err := os.ReadFile(hostKeyPath); err == nil {
|
|
return ssh.ParsePrivateKey(data)
|
|
} else if !os.IsNotExist(err) {
|
|
return nil, err
|
|
}
|
|
data := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: der})
|
|
if err := os.MkdirAll(filepath.Dir(hostKeyPath), 0o700); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := os.WriteFile(hostKeyPath, data, 0o600); err != nil {
|
|
return nil, err
|
|
}
|
|
return ssh.ParsePrivateKey(data)
|
|
}
|
|
|
|
func fingerprint(key ssh.PublicKey) string {
|
|
sum := sha256.Sum256(key.Marshal())
|
|
return "SHA256:" + hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
// normalizeName strips one trailing ".git" so both "myrepo" and "myrepo.git"
|
|
// refer to the same repo.
|
|
func normalizeName(name string) string {
|
|
return strings.TrimSuffix(name, ".git")
|
|
}
|
|
|
|
// relPath returns the repo path relative to root: "owner/name.git".
|
|
func relPath(owner, name string) string {
|
|
return owner + "/" + normalizeName(name) + ".git"
|
|
}
|
|
|
|
// findRepo looks up a repo by (owner, name). name may include ".git". A missing
|
|
// row yields ErrRepoNotFound.
|
|
func (s *LocalState) findRepo(owner, name string) (*Repo, error) {
|
|
var r Repo
|
|
has, err := s.engine.Where("namespace = ? AND name = ?", owner, normalizeName(name)).Get(&r)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("lookup repo: %w", err)
|
|
}
|
|
if !has {
|
|
return nil, ErrRepoNotFound
|
|
}
|
|
return &r, nil
|
|
}
|
|
|
|
// RepoPath returns the absolute on-disk path for owner/name, but only if the
|
|
// repo is registered in the DB -- the DB is the authority, not the filesystem.
|
|
// In skip-auth mode the DB check is skipped (any path under root resolves).
|
|
func (s *LocalState) RepoPath(owner, name string) (string, error) {
|
|
name = normalizeName(name)
|
|
rel := relPath(owner, name)
|
|
if _, err := s.findRepo(owner, name); err != nil {
|
|
return "", err
|
|
}
|
|
return common.Resolve(s.cfg.RepoRoot(), rel)
|
|
}
|
|
|
|
// Open returns a gitcmd.Repository for owner/name. The repo must be registered
|
|
// (RepoPath checks the DB) and present on disk. In skip-auth mode only the disk
|
|
// presence is required.
|
|
func (s *LocalState) Open(owner, name string) (*gitcmd.Repository, error) {
|
|
path, err := s.RepoPath(owner, name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return gitcmd.OpenRepository(context.Background(), path)
|
|
}
|
|
|
|
// Engine returns the underlying xorm engine. It is the read egress for the
|
|
// ManageService inspection RPCs (ListRepos / GetRepoGrants): those queries live
|
|
// in the gitrpc server layer, so state itself stays free of List*/management-
|
|
// read methods (only access checks + Updater writes live here). nil in skip-auth
|
|
// mode -- but ManageService is disabled under skip-auth, so no caller is reached
|
|
// with a nil engine.
|
|
func (s *LocalState) Engine() *xorm.Engine { return s.engine }
|