325 lines
8.8 KiB
Go
325 lines
8.8 KiB
Go
package gitctl
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"connectrpc.com/connect"
|
|
|
|
v1 "simplegit/gitrpc/v1"
|
|
)
|
|
|
|
type field struct {
|
|
name string
|
|
hint string
|
|
secret bool
|
|
numeric bool
|
|
optional bool
|
|
}
|
|
|
|
type action struct {
|
|
label string
|
|
confirm string
|
|
fields []field
|
|
hasPerm bool
|
|
destructive bool
|
|
immediate bool
|
|
run func(ctx context.Context, b *backend, vals map[string]string, perm string) (string, error)
|
|
}
|
|
|
|
var perms = []string{"read", "write", "admin"}
|
|
|
|
func nextPerm(p string) string {
|
|
switch p {
|
|
case "read":
|
|
return "write"
|
|
case "write":
|
|
return "admin"
|
|
default:
|
|
return "read"
|
|
}
|
|
}
|
|
|
|
func opStatus() action {
|
|
return action{
|
|
label: "Server status", immediate: true,
|
|
run: func(ctx context.Context, b *backend, _ map[string]string, _ string) (string, error) {
|
|
st, err := b.manage.Status(ctx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return formatStatus(st), nil
|
|
},
|
|
}
|
|
}
|
|
|
|
func opCreateRepo() action {
|
|
return action{
|
|
label: "Create repo",
|
|
fields: []field{{name: "ns", hint: "owner (may be nested: org/team)"}, {name: "name", hint: "repo name"}},
|
|
run: func(ctx context.Context, b *backend, v map[string]string, _ string) (string, error) {
|
|
if err := b.manage.UpsertRepo(ctx, v["ns"], v["name"], false); err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("created %s/%s", v["ns"], v["name"]), nil
|
|
},
|
|
}
|
|
}
|
|
|
|
func opCreateNS() action {
|
|
return action{
|
|
label: "New namespace",
|
|
fields: []field{{name: "ns", hint: "namespace name"}},
|
|
run: func(ctx context.Context, b *backend, v map[string]string, _ string) (string, error) {
|
|
if err := b.manage.CreateNS(ctx, v["ns"]); err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("created namespace %s", v["ns"]), nil
|
|
},
|
|
}
|
|
}
|
|
|
|
func opChangePassword(ns string) action {
|
|
return action{
|
|
label: "Change password",
|
|
fields: []field{{name: "password", hint: "new password (empty clears)", secret: true, optional: true}},
|
|
run: func(ctx context.Context, b *backend, v map[string]string, _ string) (string, error) {
|
|
if err := b.manage.UserPasswordUpsert(ctx, ns, v["password"]); err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("updated password for %s", ns), nil
|
|
},
|
|
}
|
|
}
|
|
|
|
func opRemovePassword(ns string) action {
|
|
return action{
|
|
label: "Remove password", immediate: true, destructive: true,
|
|
confirm: fmt.Sprintf("remove password login from namespace %s", ns),
|
|
run: func(ctx context.Context, b *backend, _ map[string]string, _ string) (string, error) {
|
|
if err := b.manage.UserDelete(ctx, ns); err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("removed password from %s", ns), nil
|
|
},
|
|
}
|
|
}
|
|
|
|
func opDeleteNS(ns string) action {
|
|
return action{
|
|
label: "Delete namespace", immediate: true, destructive: true,
|
|
confirm: fmt.Sprintf("delete empty namespace %s and its namespace-level grants", ns),
|
|
run: func(ctx context.Context, b *backend, _ map[string]string, _ string) (string, error) {
|
|
if err := b.manage.DeleteNS(ctx, ns); err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("deleted namespace %s", ns), nil
|
|
},
|
|
}
|
|
}
|
|
|
|
func opDeleteRepo(ns, name string) action {
|
|
return action{
|
|
label: "Delete repo", immediate: true, destructive: true,
|
|
confirm: fmt.Sprintf("delete repo %s/%s (and its repo-level grants, and the on-disk bare repo)", ns, name),
|
|
run: func(ctx context.Context, b *backend, _ map[string]string, _ string) (string, error) {
|
|
if err := b.manage.DeleteRepo(ctx, ns, name); err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("deleted %s/%s", ns, name), nil
|
|
},
|
|
}
|
|
}
|
|
|
|
func opToggleRepoVisibility(repo repoRow) action {
|
|
next := !repo.IsPrivate
|
|
visibility := "public"
|
|
if next {
|
|
visibility = "private"
|
|
}
|
|
return action{
|
|
label: "Set repo " + visibility, immediate: true,
|
|
run: func(ctx context.Context, b *backend, _ map[string]string, _ string) (string, error) {
|
|
if err := b.manage.UpsertRepo(ctx, repo.Ns, repo.Name, next); err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("set %s/%s %s", repo.Ns, repo.Name, visibility), nil
|
|
},
|
|
}
|
|
}
|
|
|
|
func opMoveRepo(src string) action {
|
|
return action{
|
|
label: "Move / rename repo",
|
|
fields: []field{{name: "dst", hint: "ns/name (new)"}},
|
|
run: func(ctx context.Context, b *backend, v map[string]string, _ string) (string, error) {
|
|
if err := b.manage.MoveRepo(ctx, src, v["dst"]); err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("moved %s -> %s", src, v["dst"]), nil
|
|
},
|
|
}
|
|
}
|
|
|
|
func opMoveNS(src string) action {
|
|
return action{
|
|
label: "Move / rename namespace",
|
|
fields: []field{{name: "dst", hint: "namespace (new)"}},
|
|
run: func(ctx context.Context, b *backend, v map[string]string, _ string) (string, error) {
|
|
if err := b.manage.MoveNS(ctx, src, v["dst"]); err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("moved namespace %s -> %s", src, v["dst"]), nil
|
|
},
|
|
}
|
|
}
|
|
|
|
func opGrantSSHKey() action {
|
|
return action{
|
|
label: "Grant SSH key",
|
|
fields: []field{{name: "target", hint: "ns/name (repo) or ns (namespace; nested ok)"}, {name: "key", hint: "ssh-ed25519 AAAA... comment"}},
|
|
hasPerm: true,
|
|
run: func(ctx context.Context, b *backend, v map[string]string, perm string) (string, error) {
|
|
return grantSSHOn(ctx, b, v["target"], v["key"], perm)
|
|
},
|
|
}
|
|
}
|
|
|
|
func opGrantPAT() action {
|
|
return action{
|
|
label: "Grant PAT",
|
|
fields: []field{{name: "target", hint: "ns/name (repo) or ns (namespace; nested ok)"}, {name: "pat", hint: "plaintext PAT", secret: true}},
|
|
hasPerm: true,
|
|
run: func(ctx context.Context, b *backend, v map[string]string, perm string) (string, error) {
|
|
return grantPATOn(ctx, b, v["target"], v["pat"], perm)
|
|
},
|
|
}
|
|
}
|
|
|
|
func grantSSHOn(ctx context.Context, b *backend, tgt, key, perm string) (string, error) {
|
|
if strings.Contains(tgt, "/") {
|
|
id, err := b.manage.ACLUpsertSSHKeyOnRepo(ctx, tgt, key, perm)
|
|
if err == nil {
|
|
return fmt.Sprintf("granted %s on repo %s (ssh, acl_id=%d)", perm, tgt, id), nil
|
|
}
|
|
if connect.CodeOf(err) != connect.CodeNotFound {
|
|
return "", err
|
|
}
|
|
}
|
|
id, err := b.manage.ACLUpsertSSHKeyOnNS(ctx, tgt, key, perm)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("granted %s on namespace %s (ssh, acl_id=%d)", perm, tgt, id), nil
|
|
}
|
|
|
|
func grantPATOn(ctx context.Context, b *backend, tgt, pat, perm string) (string, error) {
|
|
if strings.Contains(tgt, "/") {
|
|
id, err := b.manage.ACLUpsertPATOnRepo(ctx, tgt, pat, perm)
|
|
if err == nil {
|
|
return fmt.Sprintf("granted %s on repo %s (pat, acl_id=%d)", perm, tgt, id), nil
|
|
}
|
|
if connect.CodeOf(err) != connect.CodeNotFound {
|
|
return "", err
|
|
}
|
|
}
|
|
id, err := b.manage.ACLUpsertPATOnNS(ctx, tgt, pat, perm)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("granted %s on namespace %s (pat, acl_id=%d)", perm, tgt, id), nil
|
|
}
|
|
|
|
func opACLDelete(id int64) action {
|
|
return action{
|
|
label: "Delete ACL grant", immediate: true, destructive: true,
|
|
confirm: fmt.Sprintf("delete ACL grant #%d", id),
|
|
run: func(ctx context.Context, b *backend, _ map[string]string, _ string) (string, error) {
|
|
if err := b.manage.ACLDelete(ctx, id); err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("deleted ACL grant %d", id), nil
|
|
},
|
|
}
|
|
}
|
|
|
|
func opRotatePerm(g grantRow) action {
|
|
return action{
|
|
label: "Rotate grant perm", immediate: true,
|
|
run: func(ctx context.Context, b *backend, _ map[string]string, _ string) (string, error) {
|
|
next := nextPerm(g.Perm)
|
|
if err := b.manage.ACLSetPerm(ctx, g.AclID, next); err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("rotated %s grant on %s: %s -> %s", g.CredType, g.TargetLabel, g.Perm, next), nil
|
|
},
|
|
}
|
|
}
|
|
|
|
func formatStatus(st *v1.StatusResponse) string {
|
|
started := "-"
|
|
if st.StartedAt != nil {
|
|
started = fmtTime(st.StartedAt.AsTime())
|
|
}
|
|
var b strings.Builder
|
|
fmt.Fprintln(&b, "simplegit daemon")
|
|
fmt.Fprintf(&b, " root %s\n", st.Root)
|
|
fmt.Fprintf(&b, " http %s\n", orDash(st.RpcAddr))
|
|
fmt.Fprintf(&b, " ssh %s\n", orDash(st.SshAddr))
|
|
fmt.Fprintf(&b, " gitrpc %s\n", orDash(st.ManageAddr))
|
|
fmt.Fprintf(&b, " started %s (up %s)", started, fmtDuration(st.UptimeSeconds))
|
|
return b.String()
|
|
}
|
|
|
|
func grantIdentity(g grantRow) string {
|
|
switch g.CredType {
|
|
case "pat":
|
|
s := g.PatPrefix
|
|
if g.PatName != "" {
|
|
s += " (" + g.PatName + ")"
|
|
}
|
|
return s
|
|
case "ssh":
|
|
s := g.SshKeyType + " " + g.SshFingerprint
|
|
if g.SshComment != "" {
|
|
s += " (" + g.SshComment + ")"
|
|
}
|
|
return s
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func orDash(s string) string {
|
|
if s == "" {
|
|
return "-"
|
|
}
|
|
return s
|
|
}
|
|
|
|
func fmtTime(t time.Time) string {
|
|
if t.IsZero() {
|
|
return "-"
|
|
}
|
|
return t.Format("2006-01-02 15:04")
|
|
}
|
|
|
|
func fmtDuration(secs int64) string {
|
|
d := time.Duration(secs) * time.Second
|
|
if d < 0 {
|
|
d = 0
|
|
}
|
|
h := int(d.Hours())
|
|
m := int(d.Minutes()) % 60
|
|
s := int(d.Seconds()) % 60
|
|
switch {
|
|
case h > 0:
|
|
return fmt.Sprintf("%dh%dm", h, m)
|
|
case m > 0:
|
|
return fmt.Sprintf("%dm%ds", m, s)
|
|
default:
|
|
return fmt.Sprintf("%ds", s)
|
|
}
|
|
}
|