-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathidentity.go
More file actions
339 lines (294 loc) · 9.28 KB
/
Copy pathidentity.go
File metadata and controls
339 lines (294 loc) · 9.28 KB
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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
// SPDX-License-Identifier: AGPL-3.0-or-later
// This file is a thin forwarding shim into the identity sub-package. The
// re-exported types and functions exist so existing white-box tests
// (package server) can keep calling unexported names without modification.
// New tests should be written against the identity sub-package directly.
package server
import (
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"sync"
"time"
identpkg "github.com/pilot-protocol/rendezvous/identity"
)
// --- type aliases for tests in package server --------------------------------
// These are unexported aliases that forward to the identity sub-package types.
// They exist solely to satisfy zz_identity_jwt_test.go without modification.
type jwtAud = identpkg.JwtAud
type jwtClaims = identpkg.JwtClaims
type jwksKey = identpkg.JwksKey
// jwksCache is a server-local mirror of the identity sub-package's cache.
// It exists only to keep the white-box tests (package server) working
// without modification — those tests set unexported fields directly.
// New tests should use the identity sub-package's JWKSCache.
type jwksCache struct {
mu sync.RWMutex
keys []jwksKey
url string
fetchedAt time.Time
ttl time.Duration
}
const jwksCacheTTL = 5 * time.Minute
func newJWKSCache() *jwksCache {
return &jwksCache{ttl: jwksCacheTTL}
}
func (c *jwksCache) getKey(jwksURL, kid string) (*jwksKey, error) {
c.mu.RLock()
if c.url == jwksURL && time.Since(c.fetchedAt) < c.ttl && len(c.keys) > 0 {
if kid == "" {
if len(c.keys) == 1 {
key := c.keys[0]
c.mu.RUnlock()
return &key, nil
}
c.mu.RUnlock()
return nil, fmt.Errorf("JWT missing required 'kid' header; JWKS has %d keys", len(c.keys))
}
for i := range c.keys {
if c.keys[i].Kid == kid {
key := c.keys[i]
c.mu.RUnlock()
return &key, nil
}
}
c.mu.RUnlock()
return nil, fmt.Errorf("JWKS key %q not found (cached)", kid)
}
c.mu.RUnlock()
keys, err := fetchJWKSKeys(jwksURL)
if err != nil {
return nil, err
}
c.mu.Lock()
c.keys = keys
c.url = jwksURL
c.fetchedAt = time.Now()
c.mu.Unlock()
if kid == "" {
if len(keys) == 1 {
return &keys[0], nil
}
return nil, fmt.Errorf("JWT missing required 'kid' header; JWKS has %d keys", len(keys))
}
for i := range keys {
if keys[i].Kid == kid {
return &keys[i], nil
}
}
return nil, fmt.Errorf("JWKS key %q not found", kid)
}
// fetchJWKSKeys fetches the JWKS key list from the given URL.
func fetchJWKSKeys(jwksURL string) ([]jwksKey, error) {
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Get(jwksURL)
if err != nil {
return nil, fmt.Errorf("fetch JWKS: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("JWKS endpoint returned status %d", resp.StatusCode)
}
body, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024))
if err != nil {
return nil, fmt.Errorf("read JWKS: %w", err)
}
var result struct {
Keys []jwksKey `json:"keys"`
}
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("parse JWKS: %w", err)
}
return result.Keys, nil
}
// These unexported functions forward JWT operations to the identity sub-package
// so white-box tests in package server can call them without importing identpkg.
func decodeJWT(token string) (*identpkg.JwtHeader, *identpkg.JwtClaims, string, error) {
return identpkg.DecodeJWT(token)
}
func verifyJWTSignatureHS256(signingInput string, signatureB64 string, secret []byte) error {
return identpkg.VerifyJWTSignatureHS256(signingInput, signatureB64, secret)
}
func verifyJWTSignatureRS256(signingInput, signatureB64 string, key *jwksKey) error {
return identpkg.VerifyJWTSignatureRS256(signingInput, signatureB64, key)
}
func validateJWTClaims(claims *jwtClaims, expectedIssuer, expectedAudience string) error {
return identpkg.ValidateJWTClaims(claims, expectedIssuer, expectedAudience)
}
func (s *Server) verifyIdentityToken(token string) (string, error) {
return s.identity.VerifyToken(token)
}
// SetIdentityWebhookURL sets the webhook URL used to verify identity tokens.
func (s *Server) SetIdentityWebhookURL(url string) {
s.identity.SetWebhookURL(url)
}
func (s *Server) GetIdentityWebhookURL() string {
return s.identity.GetWebhookURL()
}
// SetIdentityWebhookSecret sets the HMAC-SHA256 pre-shared secret for
// identity webhook request/response signing (PILOT-240).
func (s *Server) SetIdentityWebhookSecret(secret string) {
s.identity.SetIdentityWebhookSecret(secret)
}
// GetIdentityWebhookSecret returns the current identity webhook HMAC secret.
func (s *Server) GetIdentityWebhookSecret() string {
return s.identity.GetIdentityWebhookSecret()
}
func (s *Server) provisionCallbacks() identpkg.ProvisionCallbacks {
return identpkg.ProvisionCallbacks{
FindOrCreateNetwork: s.findOrCreateNetwork,
EnableEnterprise: s.enableEnterpriseLocked,
ApplyNetworkPolicy: s.applyBlueprintPolicy,
ApplyExprPolicy: s.applyBlueprintExprPolicy,
SetAuditWebhookURL: s.SetWebhookURL,
StoreRBACPreAssignments: s.storeRBACPreAssignments,
ConfigureAuditExport: s.configureAuditExport,
IncProvisionsTotal: s.metrics.ProvisionsTotal.Inc,
}
}
func (s *Server) ApplyBlueprint(bp *identpkg.NetworkBlueprint, adminToken string) (*identpkg.ProvisionResult, error) {
return s.identity.ApplyBlueprint(bp, adminToken, s.provisionCallbacks())
}
func (s *Server) handleProvisionNetwork(msg map[string]interface{}) (map[string]interface{}, error) {
return s.identity.HandleProvisionNetwork(msg, s.authz.AdminToken(), s.provisionCallbacks())
}
func (s *Server) GetIdentityProviderConfig() *identpkg.BlueprintIdentityProvider {
return s.identity.GetIDPConfig()
}
func (s *Server) GetAuditExportConfig() *identpkg.BlueprintAuditExport {
return s.auditStore.ExporterConfig()
}
// storeIdentityProviderConfig is unexported so white-box tests can call it directly.
func (s *Server) storeIdentityProviderConfig(idp *identpkg.BlueprintIdentityProvider) {
s.identity.SetIDPConfig(idp)
}
func (s *Server) storeRBACPreAssignments(netID uint16, roles []BlueprintRole) {
s.mu.Lock()
defer s.mu.Unlock()
if s.rbacPreAssign == nil {
s.rbacPreAssign = make(map[uint16][]BlueprintRole)
}
s.rbacPreAssign[netID] = roles
}
// applyRBACPreAssignmentLocked applies any pre-assigned role to nodeID if its
// external_id matches a stored pre-assignment for netID. Caller must hold s.mu.
func (s *Server) applyRBACPreAssignmentLocked(netID uint16, nodeID uint32) {
callbacks := identpkg.RBACPreAssignCallbacks{
GetRoles: func(nid uint16, uid uint32) ([]BlueprintRole, string, bool) {
roles, ok := s.rbacPreAssign[nid]
if !ok {
return nil, "", false
}
node, ok := s.nodes[uid]
if !ok {
return nil, "", false
}
return roles, node.ExternalID, true
},
CommitRole: func(nid uint16, uid uint32, role string) {
net, ok := s.networks[nid]
if !ok {
return
}
var r Role
switch role {
case "owner":
r = RoleOwner
case "admin":
r = RoleAdmin
default:
r = RoleMember
}
net.MemberRoles[uid] = r
s.save()
},
IncCounter: s.metrics.RbacPreAssignments.Inc,
}
s.identity.ApplyRBACPreAssignment(netID, nodeID, callbacks)
}
func (s *Server) findOrCreateNetwork(name string, enterprise bool, joinRule, joinToken, networkAdminToken, adminToken string) (uint16, bool, error) {
s.mu.Lock()
defer s.mu.Unlock()
for _, net := range s.networks {
if net.Name == name {
return net.ID, false, nil
}
}
if s.authz.AdminToken() == "" {
return 0, false, fmt.Errorf("network creation disabled (no admin token)")
}
if err := s.checkAdminToken(map[string]interface{}{"admin_token": adminToken}, s.authz.AdminToken()); err != nil {
return 0, false, err
}
if err := validateNetworkName(name); err != nil {
return 0, false, err
}
netID := s.nextNet
s.nextNet++
if joinRule == "" {
joinRule = "open"
}
net := &NetworkInfo{
ID: netID,
Name: name,
Enterprise: enterprise,
Members: nil,
MemberRoles: make(map[uint32]Role),
JoinRule: joinRule,
Created: s.now(),
}
if joinToken != "" {
net.Token = joinToken
}
if networkAdminToken != "" {
net.AdminToken = networkAdminToken
}
s.networks[netID] = net
s.save()
return netID, true, nil
}
func (s *Server) enableEnterpriseLocked(netID uint16) {
s.mu.Lock()
net, ok := s.networks[netID]
if ok && !net.Enterprise {
net.Enterprise = true
s.save()
}
s.mu.Unlock()
}
func (s *Server) applyBlueprintPolicy(netID uint16, pol *identpkg.BlueprintPolicy) error {
s.mu.Lock()
defer s.mu.Unlock()
net, ok := s.networks[netID]
if !ok {
return fmt.Errorf("network %d not found", netID)
}
if pol.MaxMembers > 0 {
net.Policy.MaxMembers = pol.MaxMembers
}
if len(pol.AllowedPorts) > 0 {
net.Policy.AllowedPorts = pol.AllowedPorts
}
if pol.Description != "" {
net.Policy.Description = pol.Description
}
s.save()
return nil
}
func (s *Server) applyBlueprintExprPolicy(netID uint16, policyData json.RawMessage) error {
s.mu.Lock()
defer s.mu.Unlock()
net, ok := s.networks[netID]
if !ok {
return fmt.Errorf("network %d not found", netID)
}
net.ExprPolicy = policyData
s.save()
return nil
}
func (s *Server) configureAuditExport(cfg *identpkg.BlueprintAuditExport) {
s.auditStore.SetExporter(cfg)
slog.Info("audit export configured", "format", cfg.Format, "endpoint", cfg.Endpoint)
}