// 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) { if s.skipAuth { return true, nil } 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) { if s.skipAuth { return true, nil } 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 }