~bigbes/sr-ht-dolt

ref: cd0b0c0fb427bff6060c3f785d67784b999610f3 sr-ht-dolt/remoteapi/credsvc.go -rw-r--r-- 4.7 KiB
cd0b0c0f — Eugene Blikh web: rename a database from its settings page 3 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
package remoteapi

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"log/slog"
	"net"
	"strings"

	remotesapi "github.com/dolthub/dolt/go/gen/proto/dolt/services/remotesapi/v1alpha1"
	"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/auxilia/scribe"

	"sourcecraft.dev/bigbes/sr-ht-core/config"
	"sourcecraft.dev/bigbes/sr-ht-core/database"

	"sourcecraft.dev/bigbes/sr-ht-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      *slog.Logger
}

// 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.ErrorContext(ctx, "WhoAmI backend error", scribe.Err(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 *slog.Logger
}

// 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 := slog.Default().With("component", "credentials")

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