coverage~bigbes/sr-ht-doltede3b0bbremoteapi/credsvc.go

Coverage
0.0% 0/37 statements
Δ
Blob
ddf0718
1 package remoteapi
2
3 import (
4 "context"
5 "database/sql"
6 "errors"
7 "fmt"
8 "log/slog"
9 "net"
10 "strings"
11
12 remotesapi "github.com/dolthub/dolt/go/gen/proto/dolt/services/remotesapi/v1alpha1"
13 "github.com/vaughan0/go-ini"
14 "google.golang.org/grpc"
15 "google.golang.org/grpc/codes"
16 "google.golang.org/grpc/metadata"
17 "google.golang.org/grpc/status"
18
19 "go.bigb.es/auxilia/scribe"
20
21 "sourcecraft.dev/bigbes/sr-ht-core/config"
22 "sourcecraft.dev/bigbes/sr-ht-core/database"
23
24 "sourcecraft.dev/bigbes/sr-ht-dolt/authn"
25 )
26
27 // credService implements remotesapi.CredentialsServiceServer.WhoAmI. `dolt
28 // login` polls WhoAmI with the user's keypair Bearer JWT until the key is
29 // associated with a SourceHut account through the web UI; once association
30 // happens, the JWT verifies and WhoAmI returns the account identity, which the
31 // CLI prints as confirmation.
32 type credService struct {
33 remotesapi.UnimplementedCredentialsServiceServer
34
35 conf ini.File
36 service string
37 expectedAud string
38 pool *sql.DB
39 keys authn.KeyStore
40 logger *slog.Logger
41 }
42
43 // WhoAmI verifies the request's Bearer keypair JWT (exactly as the remotesapi
44 // interceptors do) and returns the owning SourceHut user's identity. It is
45 // keypair-only: a missing or non-Bearer authorization header, or an invalid
46 // token, is Unauthenticated — the state the CLI polls through until the web UI
47 // associates the key. A transient backend failure is Unavailable.
48 0 func (c *credService) WhoAmI(ctx context.Context, _ *remotesapi.WhoAmIRequest) (*remotesapi.WhoAmIResponse, error) {
49 0 ctx = database.Context(config.Context(ctx, c.conf, c.service), c.pool)
50 0
51 0 token, err := bearerToken(ctx)
52 0 if err != nil {
53 0 return nil, err
54 0 }
55
56 0 ac, err := authn.ResolveDoltJWT(ctx, token, c.expectedAud, c.keys)
57 0 if err != nil {
58 0 if errors.Is(err, authn.ErrInvalidToken) {
59 0 return nil, status.Error(codes.Unauthenticated, "invalid or expired credentials")
60 0 }
61 0 c.logger.ErrorContext(ctx, "WhoAmI backend error", scribe.Err(err))
62 0 return nil, status.Error(codes.Unavailable, "authentication temporarily unavailable")
63 }
64
65 // The meta mirror carries no separate display name, so we surface the
66 // username for DisplayName; EmailAddress comes from the mirrored profile.
67 0 return &remotesapi.WhoAmIResponse{
68 0 Username: ac.Username,
69 0 DisplayName: ac.Username,
70 0 EmailAddress: ac.Email,
71 0 }, nil
72 }
73
74 // bearerToken extracts the raw JWT from an incoming "authorization: Bearer <jwt>"
75 // metadata header. A missing header or non-Bearer scheme is Unauthenticated.
76 0 func bearerToken(ctx context.Context) (string, error) {
77 0 md, ok := metadata.FromIncomingContext(ctx)
78 0 if !ok {
79 0 return "", status.Error(codes.Unauthenticated, "missing credentials")
80 0 }
81 0 vals := md.Get("authorization")
82 0 if len(vals) == 0 || vals[0] == "" {
83 0 return "", status.Error(codes.Unauthenticated, "missing credentials")
84 0 }
85 0 scheme, value, found := strings.Cut(vals[0], " ")
86 0 if !found || !strings.EqualFold(scheme, "bearer") || value == "" {
87 0 return "", status.Error(codes.Unauthenticated, "expected a Bearer keypair token")
88 0 }
89 0 return value, nil
90 }
91
92 // CredServer is the small standalone gRPC server hosting CredentialsService on
93 // its own port (nginx path-routes it separately from the chunk-store server).
94 type CredServer struct {
95 grpc *grpc.Server
96 addr string
97 logger *slog.Logger
98 }
99
100 // NewCredServer assembles the CredentialsService server. It shares the keystore
101 // derivation and audience normalization with the chunk-store server so both
102 // verify keypair JWTs identically.
103 0 func NewCredServer(cfg Config) (*CredServer, error) {
104 0 if cfg.DB == nil {
105 0 return nil, fmt.Errorf("remoteapi: NewCredServer requires a non-nil DB")
106 0 }
107 0 if cfg.CredsListenAddr == "" {
108 0 return nil, fmt.Errorf("remoteapi: NewCredServer requires a CredsListenAddr")
109 0 }
110 0 logger := slog.Default().With("component", "credentials")
111 0
112 0 svc := &credService{
113 0 conf: cfg.Conf,
114 0 service: serviceName,
115 0 expectedAud: normalizeAud(cfg.HttpHost),
116 0 pool: cfg.DB,
117 0 keys: newKeyStore(cfg.DB),
118 0 logger: logger,
119 0 }
120 0
121 0 gsrv := grpc.NewServer()
122 0 remotesapi.RegisterCredentialsServiceServer(gsrv, svc)
123 0
124 0 return &CredServer{grpc: gsrv, addr: cfg.CredsListenAddr, logger: logger}, nil
125 }
126
127 // Serve binds the listener and serves until GracefulStop. It blocks. It returns
128 // an error if binding fails or the gRPC server exits with one.
129 0 func (s *CredServer) Serve() error {
130 0 lis, err := net.Listen("tcp", s.addr)
131 0 if err != nil {
132 0 return fmt.Errorf("remoteapi: bind credentials %q: %w", s.addr, err)
133 0 }
134 0 if err := s.grpc.Serve(lis); err != nil && !errors.Is(err, grpc.ErrServerStopped) {
135 0 return fmt.Errorf("remoteapi: credentials serve: %w", err)
136 0 }
137 0 return nil
138 }
139
140 // GracefulStop stops the credentials server.
141 0 func (s *CredServer) GracefulStop() { s.grpc.GracefulStop() }