Files

424 lines
15 KiB
Go

package db
import "context"
import "database/sql"
import "errors"
import "strings"
import "time"
import "codit/internal/models"
import "codit/internal/util"
const MaxPersonalGPGKeysPerUser = 10
var ErrGPGKeyAlreadyRegistered = errors.New("gpg key already registered")
var ErrPersonalGPGKeyLimitReached = errors.New("personal gpg key limit reached")
// CreateGPGKey inserts a key with the given scope. For scope 'personal',
// ownerPublicID is the owning user; for 'project', ownerProjectPublicID is the
// owning project; for 'global', both are empty.
func (s *Store) CreateGPGKey(ctx context.Context, item models.GPGKey, ownerPublicID string, ownerProjectPublicID string) (models.GPGKey, error) {
var now int64
var tx txExecutor
var owned bool
var exists bool
var personalCount int
var internalProjectID sql.NullInt64 // nullable numeric project id
var internalUserID sql.NullInt64 // nullable numeric user id
var err error
if strings.TrimSpace(item.ID) == "" {
item.ID, err = util.NewID()
if err != nil { return item, err }
}
if strings.TrimSpace(item.Scope) == "" { item.Scope = models.GPGKeyScopeGlobal }
now = time.Now().UTC().Unix()
item.CreatedAt = now
item.UpdatedAt = now
tx, owned, err = s.beginImmediateContext(ctx)
if err != nil { return item, err }
if item.Scope == models.GPGKeyScopeGlobal {
internalProjectID = sql.NullInt64{ Valid: false }
internalUserID = sql.NullInt64{ Valid: false }
} else if item.Scope == models.GPGKeyScopePersonal {
var tmpid int64
ownerPublicID = strings.TrimSpace(ownerPublicID)
if ownerPublicID == "" {
rollbackIfOwned(tx, owned)
return item, errors.New("owner user is required for personal gpg key")
}
err = tx.QueryRow(`SELECT id FROM users WHERE public_id = ?`, ownerPublicID).Scan(&tmpid)
if err != nil {
rollbackIfOwned(tx, owned)
return item, err
}
internalUserID = sql.NullInt64{ Int64: tmpid, Valid: true }
internalProjectID = sql.NullInt64{ Valid: false }
personalCount, err = countPersonalGPGKeys(tx, tmpid)
if err != nil {
rollbackIfOwned(tx, owned)
return item, err
}
if personalCount >= MaxPersonalGPGKeysPerUser {
rollbackIfOwned(tx, owned)
return item, ErrPersonalGPGKeyLimitReached
}
exists, err = personalGPGKeyFingerprintExists(tx, item.Fingerprint)
if err != nil {
rollbackIfOwned(tx, owned)
return item, err
}
if exists {
rollbackIfOwned(tx, owned)
return item, ErrGPGKeyAlreadyRegistered
}
} else if item.Scope == models.GPGKeyScopeProject {
var tmpid int64
ownerProjectPublicID = strings.TrimSpace(ownerProjectPublicID)
if ownerProjectPublicID == "" {
rollbackIfOwned(tx, owned)
return item, errors.New("owner project is required for project gpg key")
}
err = tx.QueryRow(`SELECT id FROM projects WHERE public_id = ?`, ownerProjectPublicID).Scan(&tmpid)
if err != nil {
rollbackIfOwned(tx, owned)
return item, err
}
internalProjectID = sql.NullInt64{ Int64: tmpid, Valid: true }
internalUserID = sql.NullInt64{ Valid: false }
} else {
rollbackIfOwned(tx, owned)
return item, errors.New("invalid gpg key scope")
}
_, err = tx.Exec(`INSERT INTO gpg_keys (public_id, name, fingerprint, key_id, public_key, private_key, scope, owner_user_id, owner_project_id, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
item.ID, strings.TrimSpace(item.Name), item.Fingerprint, item.KeyID, item.PublicKey, item.PrivateKey, item.Scope,
internalUserID, internalProjectID, item.CreatedAt, item.UpdatedAt)
if err != nil {
rollbackIfOwned(tx, owned)
return item, err
}
err = commitIfOwned(tx, owned)
if err != nil { return item, err }
return item, nil
}
func scanGPGKey(rows *sql.Rows) (models.GPGKey, error) {
var item models.GPGKey
var err error
err = rows.Scan(&item.ID, &item.Name, &item.Fingerprint, &item.KeyID, &item.PublicKey, &item.Scope, &item.OwnerUserID, &item.OwnerProjectID, &item.OwnerName, &item.OwnerProjectName, &item.OwnerProjectPublicID, &item.UsedByCount, &item.CreatedAt, &item.UpdatedAt)
return item, err
}
const gpgKeySelectColumns = `k.public_id, k.name, k.fingerprint, k.key_id, k.public_key, k.scope, COALESCE(k.owner_user_id, 0), COALESCE(k.owner_project_id, 0),
COALESCE(u.username, ''), COALESCE(p.name, ''), COALESCE(p.public_id, ''),
(SELECT COUNT(*) FROM repos r WHERE r.signing_key_id = k.id), k.created_at, k.updated_at`
// ListReposUsingGPGKey returns the repositories that reference the given key as
// their signing key. Used to block deletion of an in-use key and to show usage.
func (s *Store) ListReposUsingGPGKey(keyPublicID string) ([]models.GPGKeyRepoUsage, error) {
var rows *sql.Rows
var items []models.GPGKeyRepoUsage
var err error
rows, err = s.Query(`SELECT r.public_id, r.name, p.public_id, p.name
FROM repos r
JOIN projects p ON p.id = r.project_id
WHERE r.signing_key_id = (SELECT id FROM gpg_keys WHERE public_id = ?)
ORDER BY p.name COLLATE NOCASE, r.name COLLATE NOCASE`, strings.TrimSpace(keyPublicID))
if err != nil { return nil, err }
defer rows.Close()
for rows.Next() {
var item models.GPGKeyRepoUsage
err = rows.Scan(&item.RepoID, &item.RepoName, &item.ProjectID, &item.ProjectName)
if err != nil { return nil, err }
items = append(items, item)
}
return items, rows.Err()
}
// ListGPGKeys returns every key (admin view).
func (s *Store) ListGPGKeys() ([]models.GPGKey, error) {
var rows *sql.Rows
var items []models.GPGKey
var err error
rows, err = s.Query(`SELECT ` + gpgKeySelectColumns + `
FROM gpg_keys k LEFT JOIN users u ON u.id = k.owner_user_id LEFT JOIN projects p ON p.id = k.owner_project_id
ORDER BY k.name COLLATE NOCASE`)
if err != nil { return nil, err }
defer rows.Close()
for rows.Next() {
var item models.GPGKey
item, err = scanGPGKey(rows)
if err != nil { return nil, err }
items = append(items, item)
}
return items, nil
}
// ListPersonalGPGKeys returns the personal keys owned by the given user.
func (s *Store) ListPersonalGPGKeys(userPublicID string) ([]models.GPGKey, error) {
var rows *sql.Rows
var items []models.GPGKey
var err error
// [NOTE]
// we may have to add the "u.disabled = 0" to the WHERE clause if disabling a user should
// remove the trust from the system.
rows, err = s.Query(`SELECT `+gpgKeySelectColumns+`
FROM gpg_keys k LEFT JOIN users u ON u.id = k.owner_user_id LEFT JOIN projects p ON p.id = k.owner_project_id
WHERE k.scope = ? AND k.owner_user_id = (SELECT id FROM users WHERE public_id = ?)
ORDER BY k.name COLLATE NOCASE`, models.GPGKeyScopePersonal, strings.TrimSpace(userPublicID))
if err != nil { return nil, err }
defer rows.Close()
for rows.Next() {
var item models.GPGKey
item, err = scanGPGKey(rows)
if err != nil { return nil, err }
items = append(items, item)
}
return items, nil
}
// PersonalGPGKeyFingerprintExists reports whether any personal key already
// carries the given fingerprint. Personal fingerprints are globally unique so a
// key cannot be registered by multiple users.
func (s *Store) PersonalGPGKeyFingerprintExists(fingerprint string) (bool, error) {
return personalGPGKeyFingerprintExists(s, fingerprint)
}
func countPersonalGPGKeys(exec sqlExecutor, userInternalID int64) (int, error) {
var n int
var err error
err = exec.QueryRow(`SELECT COUNT(*) FROM gpg_keys WHERE scope = ? AND owner_user_id = ?`,
models.GPGKeyScopePersonal, userInternalID).Scan(&n)
if err != nil {
return 0, err
}
return n, nil
}
func personalGPGKeyFingerprintExists(exec sqlExecutor, fingerprint string) (bool, error) {
var n int
var err error
err = exec.QueryRow(`SELECT COUNT(*) FROM gpg_keys WHERE scope = ? AND fingerprint = ?`,
models.GPGKeyScopePersonal, strings.TrimSpace(fingerprint)).Scan(&n)
if err != nil { return false, err }
return n > 0, nil
}
// ListSelectableGPGKeys returns keys a project may use to sign: the project's
// own keys plus all global keys.
func (s *Store) ListSelectableGPGKeys(projectPublicID string) ([]models.GPGKey, error) {
var rows *sql.Rows
var items []models.GPGKey
var err error
rows, err = s.Query(`SELECT `+gpgKeySelectColumns+`
FROM gpg_keys k LEFT JOIN users u ON u.id = k.owner_user_id LEFT JOIN projects p ON p.id = k.owner_project_id
WHERE k.scope = ?
OR (k.scope = ? AND k.owner_project_id = (SELECT id FROM projects WHERE public_id = ?))
ORDER BY k.scope, k.name COLLATE NOCASE`, models.GPGKeyScopeGlobal, models.GPGKeyScopeProject, strings.TrimSpace(projectPublicID))
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var item models.GPGKey
item, err = scanGPGKey(rows)
if err != nil {
return nil, err
}
items = append(items, item)
}
return items, nil
}
// ListProjectGPGKeys returns the keys owned by the given project.
func (s *Store) ListProjectGPGKeys(projectPublicID string) ([]models.GPGKey, error) {
var rows *sql.Rows
var items []models.GPGKey
var err error
rows, err = s.Query(`SELECT `+gpgKeySelectColumns+`
FROM gpg_keys k LEFT JOIN users u ON u.id = k.owner_user_id LEFT JOIN projects p ON p.id = k.owner_project_id
WHERE k.scope = ? AND k.owner_project_id = (SELECT id FROM projects WHERE public_id = ?)
ORDER BY k.name COLLATE NOCASE`, models.GPGKeyScopeProject, strings.TrimSpace(projectPublicID))
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var item models.GPGKey
item, err = scanGPGKey(rows)
if err != nil {
return nil, err
}
items = append(items, item)
}
return items, nil
}
// GPGKeySelectableByProject reports whether the project may sign with the key:
// the key is either a global key or a project key owned by that project.
func (s *Store) GPGKeySelectableByProject(keyPublicID string, projectPublicID string) (bool, error) {
var n int
var err error
err = s.QueryRow(`SELECT COUNT(*) FROM gpg_keys k
WHERE k.public_id = ?
AND (k.scope = ?
OR (k.scope = ? AND k.owner_project_id = (SELECT id FROM projects WHERE public_id = ?)))`,
strings.TrimSpace(keyPublicID), models.GPGKeyScopeGlobal, models.GPGKeyScopeProject, strings.TrimSpace(projectPublicID)).Scan(&n)
if err != nil {
return false, err
}
return n > 0, nil
}
// ListPersonalVerificationKeys returns every registered personal public key
// with its owner's account identity, for verifying commit signatures.
func (s *Store) ListPersonalVerificationKeys() ([]models.PersonalVerificationKey, error) {
var rows *sql.Rows
var items []models.PersonalVerificationKey
var err error
rows, err = s.Query(`SELECT k.fingerprint, k.public_key, COALESCE(u.username, ''), COALESCE(u.email, '')
FROM gpg_keys k JOIN users u ON u.id = k.owner_user_id
WHERE k.scope = ?`, models.GPGKeyScopePersonal)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var item models.PersonalVerificationKey
err = rows.Scan(&item.Fingerprint, &item.PublicKey, &item.OwnerName, &item.OwnerEmail)
if err != nil {
return nil, err
}
items = append(items, item)
}
return items, rows.Err()
}
// ListPersonalVerificationKeysByIssuer returns registered personal keys whose
// primary fingerprint or key ID matches the detached signature issuer metadata.
func (s *Store) ListPersonalVerificationKeysByIssuer(fingerprint string, keyID string) ([]models.PersonalVerificationKey, error) {
var rows *sql.Rows
var items []models.PersonalVerificationKey
var err error
fingerprint = strings.ToUpper(strings.TrimSpace(fingerprint))
keyID = strings.ToUpper(strings.TrimSpace(keyID))
if fingerprint == "" && keyID == "" {
return nil, nil
}
rows, err = s.Query(`SELECT k.fingerprint, k.public_key, COALESCE(u.username, ''), COALESCE(u.email, '')
FROM gpg_keys k JOIN users u ON u.id = k.owner_user_id
WHERE k.scope = ?
AND ((? != '' AND UPPER(k.fingerprint) = ?) OR (? != '' AND UPPER(k.key_id) = ?))`,
models.GPGKeyScopePersonal, fingerprint, fingerprint, keyID, keyID)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var item models.PersonalVerificationKey
err = rows.Scan(&item.Fingerprint, &item.PublicKey, &item.OwnerName, &item.OwnerEmail)
if err != nil {
return nil, err
}
items = append(items, item)
}
return items, rows.Err()
}
// GPGKeyBelongsToProject reports whether the key is a project key owned by the
// given project.
func (s *Store) GPGKeyBelongsToProject(keyPublicID string, projectPublicID string) (bool, error) {
var n int
var err error
err = s.QueryRow(`SELECT COUNT(*) FROM gpg_keys k
WHERE k.public_id = ? AND k.scope = ?
AND k.owner_project_id = (SELECT id FROM projects WHERE public_id = ?) AND k.owner_project_id != 0`,
strings.TrimSpace(keyPublicID), models.GPGKeyScopeProject, strings.TrimSpace(projectPublicID)).Scan(&n)
if err != nil {
return false, err
}
return n > 0, nil
}
// GetRepoSigningMaterial returns the armored private and public key a repo is
// configured to sign with (via repos.signing_key_id). ok is false when the
// repo has no signing key. repoInternalID is the numeric repos.id.
func (s *Store) GetRepoSigningMaterial(repoInternalID int64) (string, string, bool, error) {
var priv string
var pub string
var err error
err = s.QueryRow(`SELECT COALESCE(g.private_key, ''), COALESCE(g.public_key, '')
FROM repos r LEFT JOIN gpg_keys g ON g.id = r.signing_key_id
WHERE r.id = ?`, repoInternalID).Scan(&priv, &pub)
if err != nil {
if err == sql.ErrNoRows {
return "", "", false, nil
}
return "", "", false, err
}
return priv, pub, strings.TrimSpace(priv) != "", nil
}
func (s *Store) GetGPGKey(id string) (models.GPGKey, error) {
var row *sql.Row
var item models.GPGKey
var err error
row = s.QueryRow(`SELECT k.public_id, k.name, k.fingerprint, k.key_id, k.public_key, k.private_key, k.scope, COALESCE(k.owner_user_id, 0), COALESCE(k.owner_project_id, 0),
COALESCE(u.username, ''), COALESCE(p.name, ''), COALESCE(p.public_id, ''), k.created_at, k.updated_at
FROM gpg_keys k LEFT JOIN users u ON u.id = k.owner_user_id LEFT JOIN projects p ON p.id = k.owner_project_id
WHERE k.public_id = ?`, strings.TrimSpace(id))
err = row.Scan(&item.ID, &item.Name, &item.Fingerprint, &item.KeyID, &item.PublicKey, &item.PrivateKey, &item.Scope, &item.OwnerUserID, &item.OwnerProjectID, &item.OwnerName, &item.OwnerProjectName, &item.OwnerProjectPublicID, &item.CreatedAt, &item.UpdatedAt)
if err != nil {
return item, err
}
return item, nil
}
func (s *Store) DeleteGPGKey(id string) error {
var err error
var result sql.Result
var rowsAffected int64
result, err = s.Exec(`DELETE FROM gpg_keys WHERE public_id = ?`, strings.TrimSpace(id))
if err != nil { return err }
rowsAffected, err = result.RowsAffected()
if err != nil { return err }
if rowsAffected <= 0 { return sql.ErrNoRows }
return err
}
// GPGKeyPersonalOwner reports whether the personal key is owned by the user.
func (s *Store) GPGKeyPersonalOwner(keyPublicID string, userPublicID string) (bool, error) {
var n int
var err error
err = s.QueryRow(`SELECT COUNT(*) FROM gpg_keys k
WHERE k.public_id = ? AND k.scope = ?
AND k.owner_user_id = (SELECT id FROM users WHERE public_id = ?) AND k.owner_user_id != 0`,
strings.TrimSpace(keyPublicID), models.GPGKeyScopePersonal, strings.TrimSpace(userPublicID)).Scan(&n)
if err != nil {
return false, err
}
return n > 0, nil
}