| 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() } |