352 lines
9.7 KiB
Go
352 lines
9.7 KiB
Go
// Package ctrl exposes a small, named set of read-only state database
|
|
// operations for remote management clients.
|
|
package ctrl
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"xorm.io/xorm"
|
|
|
|
"simplegit/state"
|
|
)
|
|
|
|
const (
|
|
OpListRepos = "list-repos"
|
|
OpListNS = "list-ns"
|
|
OpGrantsForRepo = "grants-for-repo"
|
|
OpGrantsForNS = "grants-for-ns"
|
|
OpListGrants = "list-grants"
|
|
OpGrantsForKey = "grants-for-key"
|
|
OpListSSHKeys = "list-ssh-keys"
|
|
OpSSHKeyByID = "ssh-key-by-id"
|
|
)
|
|
|
|
var (
|
|
ErrUnknownOp = errors.New("unknown ctrl operation")
|
|
ErrInvalidArgs = errors.New("invalid ctrl arguments")
|
|
)
|
|
|
|
type engineProvider interface {
|
|
Engine() *xorm.Engine
|
|
}
|
|
|
|
type Controller struct{ eng *xorm.Engine }
|
|
|
|
func New(provider engineProvider) *Controller { return &Controller{eng: provider.Engine()} }
|
|
|
|
type RepoRow struct {
|
|
ID int64 `json:"id"`
|
|
NamespaceID int64 `json:"namespace_id"`
|
|
Ns string `json:"ns"`
|
|
Name string `json:"name"`
|
|
IsPrivate bool `json:"is_private"`
|
|
Created time.Time `json:"created"`
|
|
Updated time.Time `json:"updated"`
|
|
}
|
|
|
|
type NSRow struct {
|
|
ID int64 `json:"id"`
|
|
NamespaceID int64 `json:"namespace_id"`
|
|
Name string `json:"name"`
|
|
RepoCount int64 `json:"repo_count"`
|
|
Created time.Time `json:"created"`
|
|
Updated time.Time `json:"updated"`
|
|
HasPassword bool `json:"has_password"`
|
|
}
|
|
|
|
type GrantRow struct {
|
|
AclID int64 `json:"acl_id"`
|
|
CredType string `json:"cred_type"`
|
|
Target string `json:"target"`
|
|
Perm string `json:"perm"`
|
|
BoundAt time.Time `json:"bound_at"`
|
|
PatPrefix string `json:"pat_prefix"`
|
|
PatName string `json:"pat_name"`
|
|
SshFingerprint string `json:"ssh_fingerprint"`
|
|
SshKeyType string `json:"ssh_key_type"`
|
|
SshComment string `json:"ssh_comment"`
|
|
CredID int64 `json:"cred_id"`
|
|
TargetTypeID int `json:"target_type_id"`
|
|
TargetID int64 `json:"target_id"`
|
|
TargetLabel string `json:"target_label"`
|
|
TargetIsRepo bool `json:"target_is_repo"`
|
|
Ns string `json:"ns"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
type SSHKeyRow struct {
|
|
ID int64 `json:"id"`
|
|
Fingerprint string `json:"fingerprint"`
|
|
KeyType string `json:"key_type"`
|
|
Comment string `json:"comment"`
|
|
LastUsedAt *time.Time `json:"last_used_at"`
|
|
GrantCount int64 `json:"grant_count"`
|
|
PublicKey string `json:"public_key"`
|
|
}
|
|
|
|
func (c *Controller) Call(op string, args ...string) (json.RawMessage, error) {
|
|
var value any
|
|
var err error
|
|
switch op {
|
|
case OpListRepos:
|
|
if err = argc(op, args, 0); err == nil {
|
|
value, err = c.listRepos()
|
|
}
|
|
case OpListNS:
|
|
if err = argc(op, args, 0); err == nil {
|
|
value, err = c.listNS()
|
|
}
|
|
case OpGrantsForRepo:
|
|
if err = argc(op, args, 2); err == nil {
|
|
value, err = c.grantsForRepo(args[0], args[1])
|
|
}
|
|
case OpGrantsForNS:
|
|
if err = argc(op, args, 1); err == nil {
|
|
value, err = c.grantsForNS(args[0])
|
|
}
|
|
case OpListGrants:
|
|
if err = argc(op, args, 0); err == nil {
|
|
value, err = c.listGrants()
|
|
}
|
|
case OpGrantsForKey, OpSSHKeyByID:
|
|
if err = argc(op, args, 1); err != nil {
|
|
break
|
|
}
|
|
var id int64
|
|
id, err = strconv.ParseInt(args[0], 10, 64)
|
|
if err != nil {
|
|
err = fmt.Errorf("%w: %s expects an integer id", ErrInvalidArgs, op)
|
|
break
|
|
}
|
|
if op == OpGrantsForKey {
|
|
value, err = c.grantsForKey(id)
|
|
} else {
|
|
value, err = c.sshKeyByID(id)
|
|
}
|
|
case OpListSSHKeys:
|
|
if err = argc(op, args, 0); err == nil {
|
|
value, err = c.listSSHKeys()
|
|
}
|
|
default:
|
|
err = fmt.Errorf("%w: %q", ErrUnknownOp, op)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
data, err := json.Marshal(value)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("encode ctrl result: %w", err)
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
func argc(op string, args []string, want int) error {
|
|
if len(args) != want {
|
|
return fmt.Errorf("%w: %s expects %d arguments, got %d", ErrInvalidArgs, op, want, len(args))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Controller) listRepos() ([]RepoRow, error) {
|
|
var rows []struct {
|
|
state.Repo `xorm:"extends"`
|
|
NSName string `xorm:"ns_name"`
|
|
}
|
|
if err := c.eng.Table("repo").Select("repo.*, namespace.name AS ns_name").Join("INNER", "namespace", "namespace.id = repo.namespace_id").Asc("namespace.name", "repo.name").Find(&rows); err != nil {
|
|
return nil, err
|
|
}
|
|
out := make([]RepoRow, 0, len(rows))
|
|
for i := range rows {
|
|
r := &rows[i]
|
|
out = append(out, RepoRow{ID: r.ID, NamespaceID: r.NamespaceID, Ns: r.NSName, Name: r.Name, IsPrivate: r.IsPrivate, Created: r.CreatedAt, Updated: r.UpdatedAt})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (c *Controller) listNS() ([]NSRow, error) {
|
|
var rows []state.Namespace
|
|
if err := c.eng.Asc("name").Find(&rows); err != nil {
|
|
return nil, err
|
|
}
|
|
out := make([]NSRow, 0, len(rows))
|
|
for i := range rows {
|
|
n := &rows[i]
|
|
count, err := c.eng.Where("namespace_id = ?", n.ID).Count(&state.Repo{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, NSRow{NamespaceID: n.ID, Name: n.Name, RepoCount: count, Created: n.CreatedAt, Updated: n.UpdatedAt, HasPassword: n.PasswordHash != ""})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (c *Controller) grantsForRepo(ns, name string) ([]GrantRow, error) {
|
|
name = strings.TrimSuffix(name, ".git")
|
|
var repo state.Repo
|
|
has, err := c.eng.Where("namespace_id IN (SELECT id FROM namespace WHERE name = ?) AND name = ?", ns, name).Get(&repo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !has {
|
|
return nil, state.ErrRepoNotFound
|
|
}
|
|
var rows []state.ACL
|
|
if err := c.eng.Where("(target_type = ? AND target_id = ?) OR (target_type = ? AND target_id = ?)", state.TargetTypeRepo, repo.ID, state.TargetTypeNS, repo.NamespaceID).Find(&rows); err != nil {
|
|
return nil, err
|
|
}
|
|
return c.loadGrants(rows)
|
|
}
|
|
|
|
func (c *Controller) grantsForNS(ns string) ([]GrantRow, error) {
|
|
var n state.Namespace
|
|
has, err := c.eng.Where("name = ?", ns).Get(&n)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !has {
|
|
return nil, state.ErrNamespaceNotFound
|
|
}
|
|
var repos []state.Repo
|
|
if err := c.eng.Where("namespace_id = ?", n.ID).Find(&repos); err != nil {
|
|
return nil, err
|
|
}
|
|
var rows []state.ACL
|
|
if err := c.eng.Where("target_type = ? AND target_id = ?", state.TargetTypeNS, n.ID).Find(&rows); err != nil {
|
|
return nil, err
|
|
}
|
|
for i := range repos {
|
|
var rr []state.ACL
|
|
if err := c.eng.Where("target_type = ? AND target_id = ?", state.TargetTypeRepo, repos[i].ID).Find(&rr); err != nil {
|
|
return nil, err
|
|
}
|
|
rows = append(rows, rr...)
|
|
}
|
|
return c.loadGrants(rows)
|
|
}
|
|
|
|
func (c *Controller) listGrants() ([]GrantRow, error) {
|
|
var rows []state.ACL
|
|
if err := c.eng.Find(&rows); err != nil {
|
|
return nil, err
|
|
}
|
|
return c.loadGrants(rows)
|
|
}
|
|
func (c *Controller) grantsForKey(id int64) ([]GrantRow, error) {
|
|
var rows []state.ACL
|
|
if err := c.eng.Where("cred_type = ? AND cred_id = ?", state.CredTypeSSH, id).Find(&rows); err != nil {
|
|
return nil, err
|
|
}
|
|
return c.loadGrants(rows)
|
|
}
|
|
|
|
func (c *Controller) listSSHKeys() ([]SSHKeyRow, error) {
|
|
var keys []state.SSHKey
|
|
if err := c.eng.Find(&keys); err != nil {
|
|
return nil, err
|
|
}
|
|
out := make([]SSHKeyRow, 0, len(keys))
|
|
for i := range keys {
|
|
k := &keys[i]
|
|
count, err := c.eng.Where("cred_type = ? AND cred_id = ?", state.CredTypeSSH, k.ID).Count(&state.ACL{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, SSHKeyRow{ID: k.ID, Fingerprint: k.Fingerprint, KeyType: k.KeyType, Comment: k.Comment, LastUsedAt: k.LastUsedAt, GrantCount: count, PublicKey: k.PublicKey})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (c *Controller) sshKeyByID(id int64) (SSHKeyRow, error) {
|
|
var k state.SSHKey
|
|
has, err := c.eng.ID(id).Get(&k)
|
|
if err != nil {
|
|
return SSHKeyRow{}, err
|
|
}
|
|
if !has {
|
|
return SSHKeyRow{}, fmt.Errorf("ssh key #%d not found", id)
|
|
}
|
|
return SSHKeyRow{ID: k.ID, Fingerprint: k.Fingerprint, KeyType: k.KeyType, Comment: k.Comment, LastUsedAt: k.LastUsedAt, PublicKey: k.PublicKey}, nil
|
|
}
|
|
|
|
func (c *Controller) loadGrants(rows []state.ACL) ([]GrantRow, error) {
|
|
out := make([]GrantRow, 0, len(rows))
|
|
for i := range rows {
|
|
a := &rows[i]
|
|
g := GrantRow{AclID: a.ID, Perm: a.Perm, BoundAt: a.CreatedAt, Target: targetName(a.TargetType), CredType: credTypeName(a.CredType), CredID: a.CredID, TargetTypeID: a.TargetType, TargetID: a.TargetID}
|
|
switch a.CredType {
|
|
case int(state.CredTypePAT):
|
|
var p state.PAT
|
|
if has, _ := c.eng.ID(a.CredID).Get(&p); has {
|
|
g.PatPrefix = p.Prefix
|
|
g.PatName = p.Name
|
|
}
|
|
case int(state.CredTypeSSH):
|
|
var k state.SSHKey
|
|
if has, _ := c.eng.ID(a.CredID).Get(&k); has {
|
|
g.SshFingerprint = k.Fingerprint
|
|
g.SshKeyType = k.KeyType
|
|
g.SshComment = k.Comment
|
|
}
|
|
}
|
|
label, isRepo, ns, name, err := c.resolveTarget(a.TargetType, a.TargetID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
g.TargetLabel = label
|
|
g.TargetIsRepo = isRepo
|
|
g.Ns = ns
|
|
g.Name = name
|
|
out = append(out, g)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (c *Controller) resolveTarget(kind int, id int64) (string, bool, string, string, error) {
|
|
if kind == int(state.TargetTypeRepo) {
|
|
var r state.Repo
|
|
has, err := c.eng.ID(id).Get(&r)
|
|
if err != nil {
|
|
return "", false, "", "", err
|
|
}
|
|
if !has {
|
|
return "(deleted repo)", true, "", "", nil
|
|
}
|
|
var n state.Namespace
|
|
has, err = c.eng.ID(r.NamespaceID).Get(&n)
|
|
if err != nil {
|
|
return "", false, "", "", err
|
|
}
|
|
if !has {
|
|
return "(deleted namespace)/" + r.Name, true, "", r.Name, nil
|
|
}
|
|
return n.Name + "/" + r.Name, true, n.Name, r.Name, nil
|
|
}
|
|
var n state.Namespace
|
|
has, err := c.eng.ID(id).Get(&n)
|
|
if err != nil {
|
|
return "", false, "", "", err
|
|
}
|
|
if !has {
|
|
return "(deleted namespace)", false, "", "", nil
|
|
}
|
|
return n.Name + "/*", false, n.Name, "", nil
|
|
}
|
|
|
|
func credTypeName(v int) string {
|
|
if v == int(state.CredTypeSSH) {
|
|
return "ssh"
|
|
}
|
|
return "pat"
|
|
}
|
|
func targetName(v int) string {
|
|
if v == int(state.TargetTypeNS) {
|
|
return "ns"
|
|
}
|
|
return "repo"
|
|
}
|