package remoteapi
import (
"context"
"database/sql"
"errors"
"fmt"
"net"
"strings"
"git.sr.ht/~sircmpwn/core-go/config"
"git.sr.ht/~sircmpwn/core-go/database"
remotesapi "github.com/dolthub/dolt/go/gen/proto/dolt/services/remotesapi/v1alpha1"
"github.com/sirupsen/logrus"
"github.com/vaughan0/go-ini"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"go.bigb.es/sourcehut-dolt/authn"
)
// credService implements remotesapi.CredentialsServiceServer.WhoAmI. `dolt
// login` polls WhoAmI with the user's keypair Bearer JWT until the key is
// associated with a SourceHut account through the web UI; once association
// happens, the JWT verifies and WhoAmI returns the account identity, which the
// CLI prints as confirmation.
type credService struct {
remotesapi.UnimplementedCredentialsServiceServer
conf ini.File
service string
expectedAud string
pool *sql.DB
keys authn.KeyStore
logger *logrus.Entry
}
// WhoAmI verifies the request's Bearer keypair JWT (exactly as the remotesapi
// interceptors do) and returns the owning SourceHut user's identity. It is
// keypair-only: a missing or non-Bearer authorization header, or an invalid
// token, is Unauthenticated — the state the CLI polls through until the web UI
// associates the key. A transient backend failure is Unavailable.
func (c *credService) WhoAmI(ctx context.Context, _ *remotesapi.WhoAmIRequest) (*remotesapi.WhoAmIResponse, error) {
ctx = database.Context(config.Context(ctx, c.conf, c.service), c.pool)
token, err := bearerToken(ctx)
if err != nil {
return nil, err
}
ac, err := authn.ResolveDoltJWT(ctx, token, c.expectedAud, c.keys)
if err != nil {
if errors.Is(err, authn.ErrInvalidToken) {
return nil, status.Error(codes.Unauthenticated, "invalid or expired credentials")
}
c.logger.Errorf("credentials WhoAmI backend error: %v", err)
return nil, status.Error(codes.Unavailable, "authentication temporarily unavailable")
}
// The meta mirror carries no separate display name, so we surface the
// username for DisplayName; EmailAddress comes from the mirrored profile.
return &remotesapi.WhoAmIResponse{
Username: ac.Username,
DisplayName: ac.Username,
EmailAddress: ac.Email,
}, nil
}
// bearerToken extracts the raw JWT from an incoming "authorization: Bearer <jwt>"
// metadata header. A missing header or non-Bearer scheme is Unauthenticated.
func bearerToken(ctx context.Context) (string, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return "", status.Error(codes.Unauthenticated, "missing credentials")
}
vals := md.Get("authorization")
if len(vals) == 0 || vals[0] == "" {
return "", status.Error(codes.Unauthenticated, "missing credentials")
}
scheme, value, found := strings.Cut(vals[0], " ")
if !found || !strings.EqualFold(scheme, "bearer") || value == "" {
return "", status.Error(codes.Unauthenticated, "expected a Bearer keypair token")
}
return value, nil
}
// CredServer is the small standalone gRPC server hosting CredentialsService on
// its own port (nginx path-routes it separately from the chunk-store server).
type CredServer struct {
grpc *grpc.Server
addr string
logger *logrus.Entry
}
// NewCredServer assembles the CredentialsService server. It shares the keystore
// derivation and audience normalization with the chunk-store server so both
// verify keypair JWTs identically.
func NewCredServer(cfg Config) (*CredServer, error) {
if cfg.DB == nil {
return nil, fmt.Errorf("remoteapi: NewCredServer requires a non-nil DB")
}
if cfg.CredsListenAddr == "" {
return nil, fmt.Errorf("remoteapi: NewCredServer requires a CredsListenAddr")
}
logger := cfg.Logger
if logger == nil {
logger = logrus.NewEntry(logrus.StandardLogger())
}
svc := &credService{
conf: cfg.Conf,
service: serviceName,
expectedAud: normalizeAud(cfg.HttpHost),
pool: cfg.DB,
keys: newKeyStore(cfg.DB),
logger: logger,
}
gsrv := grpc.NewServer()
remotesapi.RegisterCredentialsServiceServer(gsrv, svc)
return &CredServer{grpc: gsrv, addr: cfg.CredsListenAddr, logger: logger}, nil
}
// Serve binds the listener and serves until GracefulStop. It blocks. It returns
// an error if binding fails or the gRPC server exits with one.
func (s *CredServer) Serve() error {
lis, err := net.Listen("tcp", s.addr)
if err != nil {
return fmt.Errorf("remoteapi: bind credentials %q: %w", s.addr, err)
}
if err := s.grpc.Serve(lis); err != nil && !errors.Is(err, grpc.ErrServerStopped) {
return fmt.Errorf("remoteapi: credentials serve: %w", err)
}
return nil
}
// GracefulStop stops the credentials server.
func (s *CredServer) GracefulStop() { s.grpc.GracefulStop() }