~bigbes/sr-ht-dolt

ref: 88a1d379f707062ebac734104a35a83065ef6b78 sr-ht-dolt/remoteapi/credsvc.go -rw-r--r-- 4.7 KiB
88a1d379 — Eugene Blikh feat(web/beads): filters for the parade board 30 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"
	"net"
	"strings"

	"sourcecraft.dev/bigbes/sr-ht-core/config"
	"sourcecraft.dev/bigbes/sr-ht-core/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"

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