| 1 |
|
package authn |
| 2 |
|
|
| 3 |
|
import ( |
| 4 |
|
"context" |
| 5 |
|
"errors" |
| 6 |
|
"strings" |
| 7 |
|
|
| 8 |
|
"sourcecraft.dev/bigbes/sr-ht-core/auth" |
| 9 |
|
) |
| 10 |
|
|
| 11 |
|
// ErrInvalidToken is the sentinel wrapped by every *permanent* credential |
| 12 |
|
// rejection: a malformed/expired/forged token, a username mismatch, or a |
| 13 |
|
// revoked token. Callers (e.g. the remotesapi interceptors) map errors.Is(err, |
| 14 |
|
// ErrInvalidToken) to an authentication failure (HTTP 401 / gRPC |
| 15 |
|
// Unauthenticated). A resolution error that does NOT wrap ErrInvalidToken is a |
| 16 |
|
// *transient* backend failure (meta.sr.ht unreachable, database error) and |
| 17 |
|
// should be surfaced as "try again later" (HTTP 500 / gRPC Unavailable), never |
| 18 |
|
// as a hard credential rejection — this mirrors core-go's auth.OAuth2, which |
| 19 |
|
// distinguishes its temporary-error path from an invalid-token path. |
| 20 |
|
var ErrInvalidToken = errors.New("authn: invalid or expired credentials") |
| 21 |
|
|
| 22 |
|
// MetaBackend abstracts the two meta.sr.ht-backed lookups the resolvers need: |
| 23 |
|
// mirroring a user into the local "user" table and checking whether a token has |
| 24 |
|
// been revoked. The production implementation (coreMetaBackend) delegates to |
| 25 |
|
// core-go, which reads the database and config from the request context; tests |
| 26 |
|
// swap in an in-memory stub so they need neither Postgres nor the network. |
| 27 |
|
type MetaBackend interface { |
| 28 |
|
// LookupUser fills out with the user identified by username, mirroring the |
| 29 |
|
// profile from meta.sr.ht into the local database on first sight. Mirrors |
| 30 |
|
// core-go's auth.LookupUser semantics. |
| 31 |
|
LookupUser(ctx context.Context, username string, out *auth.AuthContext) error |
| 32 |
|
// IsRevoked reports whether the personal access token with the given sha512 |
| 33 |
|
// hash (and, for OAuth clients, clientID) has been revoked on meta.sr.ht. |
| 34 |
|
// Mirrors core-go's auth.LookupTokenRevocation. |
| 35 |
|
IsRevoked(ctx context.Context, username string, hash [64]byte, clientID string) (bool, error) |
| 36 |
|
} |
| 37 |
|
|
| 38 |
|
// coreMetaBackend is the production MetaBackend: it forwards to core-go, whose |
| 39 |
|
// implementations read database.ForContext / config.ServiceName from ctx and |
| 40 |
|
// (on a local miss) fetch the profile from meta.sr.ht over internal GraphQL. |
| 41 |
|
type coreMetaBackend struct{} |
| 42 |
|
|
| 43 |
0 |
func (coreMetaBackend) LookupUser(ctx context.Context, username string, out *auth.AuthContext) error { |
| 44 |
0 |
return auth.LookupUser(ctx, username, out) |
| 45 |
0 |
} |
| 46 |
|
|
| 47 |
0 |
func (coreMetaBackend) IsRevoked(ctx context.Context, username string, hash [64]byte, clientID string) (bool, error) { |
| 48 |
0 |
return auth.LookupTokenRevocation(ctx, username, hash, clientID) |
| 49 |
0 |
} |
| 50 |
|
|
| 51 |
|
// meta is the backend used by the resolution functions. It defaults to the real |
| 52 |
|
// meta.sr.ht implementation; tests reassign it (white-box) and restore it. |
| 53 |
|
var meta MetaBackend = coreMetaBackend{} |
| 54 |
|
|
| 55 |
|
// SetMetaBackend swaps the package-level meta backend used by the resolvers and |
| 56 |
|
// returns a function that restores the previous one. It is a wiring/test seam: |
| 57 |
|
// integration tests living in OTHER packages (e.g. remoteapi) need to inject an |
| 58 |
|
// in-memory MetaBackend so they can exercise the full auth stack without a live |
| 59 |
|
// meta.sr.ht or the internal-network trust it requires. Production code never |
| 60 |
|
// calls it, and it is not safe for concurrent use — a test installs a backend, |
| 61 |
|
// runs, and restores it via the returned func (typically with t.Cleanup). |
| 62 |
0 |
func SetMetaBackend(b MetaBackend) (restore func()) { |
| 63 |
0 |
prev := meta |
| 64 |
0 |
meta = b |
| 65 |
0 |
return func() { meta = prev } |
| 66 |
|
} |
| 67 |
|
|
| 68 |
|
// equalUsername reports whether two SourceHut usernames refer to the same user, |
| 69 |
|
// ignoring a leading "~" (the canonical-name sigil) and ASCII case. |
| 70 |
21 |
func equalUsername(a, b string) bool { |
| 71 |
21 |
return strings.EqualFold(strings.TrimPrefix(a, "~"), strings.TrimPrefix(b, "~")) |
| 72 |
21 |
} |