114 lines
3.8 KiB
Go
114 lines
3.8 KiB
Go
// SSH-transport (public key) access checks.
|
|
//
|
|
// An SSH key is matched at request time by fingerprint. AccessBySSHKey resolves
|
|
// a presented key to an SSHKey row and checks it against the repo's (or its
|
|
// namespace's) ACL -- the state-layer half of the IState.AccessBySSHKey
|
|
// contract. SSH has no anonymous path: the key must be registered, else
|
|
// ErrDenied.
|
|
//
|
|
// SSH keys are created via ACLUpsertSSHKeyOnNS/OnRepo (IStateMut); there is no
|
|
// separate AddSSHKey and no list/management read surface.
|
|
|
|
package state
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"golang.org/x/crypto/ssh"
|
|
|
|
"simplegit/common"
|
|
)
|
|
|
|
// AccessBySSHKey authorizes key for perm on owner/name. SSH has no anonymous
|
|
// path: the key must be registered, else ErrDenied. A registered key may read a
|
|
// public repo without a grant; anything else needs an ACL grant (on the repo or
|
|
// its namespace) whose rank satisfies perm. Returns (true, nil) on allow;
|
|
// (false, ErrDenied) on an unknown key, unknown repo, or insufficient grant.
|
|
// skip-auth allows all.
|
|
func (s *LocalState) AccessBySSHKey(ctx context.Context, key ssh.PublicKey, owner, name string, perm common.Perm) (bool, error) {
|
|
r, err := s.findRepo(owner, name)
|
|
if err != nil {
|
|
if errors.Is(err, ErrRepoNotFound) {
|
|
return false, ErrDenied // never leak existence
|
|
}
|
|
return false, err
|
|
}
|
|
|
|
var sk SSHKey
|
|
has, err := s.engine.Where("fingerprint = ?", fingerprint(key)).Get(&sk)
|
|
if err != nil {
|
|
return false, fmt.Errorf("lookup ssh key: %w", err)
|
|
}
|
|
if !has {
|
|
return false, ErrDenied // unknown key
|
|
}
|
|
// best-effort last-used stamp; must not block access
|
|
now := time.Now()
|
|
if _, err := s.engine.ID(sk.ID).Cols("last_used_at").Update(&SSHKey{LastUsedAt: &now}); err == nil {
|
|
sk.LastUsedAt = &now
|
|
}
|
|
|
|
// public read is open to any registered key
|
|
if perm == common.PermRead && !r.IsPrivate {
|
|
return true, nil
|
|
}
|
|
ok, err := s.hasAccess(CredTypeSSH, sk.ID, r.ID, r.NamespaceID, perm)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if !ok {
|
|
return false, ErrDenied
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
// KnownSSHKey reports whether key is an admissible SSH credential: it must be
|
|
// registered AND carry at least one ACL grant. A registered key with no grant
|
|
// (an orphan -- e.g. its grant was ACLDelete'd, or its repo was DeleteRepo'd)
|
|
// is rejected here, at the PublicKeyCallback, so the SSH client falls through
|
|
// to another offered key that does have a grant.
|
|
//
|
|
// This mirrors Gitea's model where every registered key belongs to a user
|
|
// (principal): a key without a principal can never authorize anything, so it
|
|
// is not admitted. simplegit's namespace plays the principal role -- keys
|
|
// granted on a namespace are all equivalent for that namespace's repos, so
|
|
// which one the client offers first no longer matters (the multi-key footgun).
|
|
// SSH access therefore requires a grant; public repos stay anonymously
|
|
// readable over HTTP.
|
|
//
|
|
// skip-auth admits everything (no engine).
|
|
func (s *LocalState) KnownSSHKey(key ssh.PublicKey) (bool, error) {
|
|
var sk SSHKey
|
|
has, err := s.engine.Where("fingerprint = ?", fingerprint(key)).Get(&sk)
|
|
if err != nil {
|
|
return false, fmt.Errorf("lookup ssh key: %w", err)
|
|
}
|
|
if !has {
|
|
return false, ErrDenied // unknown key
|
|
}
|
|
// Admit only keys with at least one grant. A grant-less registered key can
|
|
// never pass AccessBySSHKey, and admitting it would block the client from
|
|
// trying a granted key.
|
|
hasGrant, err := s.keyHasGrant(sk.ID)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if !hasGrant {
|
|
return false, ErrDenied
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
// keyHasGrant reports whether SSH key credID has any ACL grant (repo or
|
|
// namespace), regardless of perm.
|
|
func (s *LocalState) keyHasGrant(credID int64) (bool, error) {
|
|
count, err := s.engine.Where("cred_type = ? AND cred_id = ?", CredTypeSSH, credID).Count(&ACL{})
|
|
if err != nil {
|
|
return false, fmt.Errorf("count ssh key grants: %w", err)
|
|
}
|
|
return count > 0, nil
|
|
}
|