1 package services
2
3 import (
4 "context"
5 "errors"
6 "fmt"
7 "net/http"
8 "time"
9
10 "github.com/gin-gonic/gin"
11 corev1 "k8s.io/api/core/v1"
12 k8serrors "k8s.io/apimachinery/pkg/api/errors"
13 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
14 "k8s.io/apimachinery/pkg/types"
15 "sigs.k8s.io/controller-runtime/pkg/client"
16
17 cmApi "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
18 cmMeta "github.com/cert-manager/cert-manager/pkg/apis/meta/v1"
19
20 api "edge-infra.dev/pkg/edge/device-registrar/api/v1alpha1"
21 "edge-infra.dev/pkg/edge/device-registrar/config"
22 clientApi "edge-infra.dev/pkg/edge/iam/api/v1alpha1"
23 )
24
25
26 type DeviceConnectResponse struct {
27 Applications []ApplicationResponse `json:"applications"`
28 DeviceAuth DeviceAuth `json:"deviceAuth,omitempty"`
29 HostMapping HostMapping `json:"hostMapping"`
30 }
31
32
33 type DeviceConnectRequest struct {
34 Device api.ExternalDeviceSpec `json:"device" binding:"required"`
35 ApplicationIDs []string `json:"applicationIDs" binding:"required,min=1"`
36 }
37
38
39
40 type ApplicationResponse struct {
41
42 ID string `json:"id"`
43
44 Name string `json:"name"`
45
46 Config EdgeIDClient `json:"config"`
47 }
48
49
50
51 type EdgeIDClient struct {
52
53 ClientID string `json:"clientID"`
54
55 ClientSecret string `json:"clientSecret"`
56 }
57
58
59
60 type DeviceAuth struct {
61 Crt []byte `json:"tls.crt,omitempty"`
62 Key []byte `json:"tls.key,omitempty"`
63 }
64
65 var errDeviceBindingNotFound = errors.New("invalid activation code")
66 var errActivationCodeNotFound = errors.New("activationCode is required")
67 var errActivationCodeExpired = errors.New("activation code has expired")
68 var errActivationCodeUsed = errors.New("activation code has already been used")
69
70
71
72
73
74
75
76
77
78
79
80 func ConnectDevice(c *gin.Context) {
81 req, err := checkRequest(c)
82 if err != nil {
83 c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
84 return
85 }
86
87 k8sClient, ctx, cancel := config.GetClientandContext(c)
88 defer cancel()
89
90
91 deviceBinding, err := retrieveDeviceRegistration(ctx, k8sClient, c)
92 if err == errActivationCodeNotFound {
93 c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
94 return
95 } else if err != nil {
96 c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
97 return
98 }
99
100 if err := register(ctx, k8sClient, c, req, deviceBinding); err != nil {
101 return
102 }
103
104
105 devb := &api.DeviceBinding{}
106 if err := k8sClient.Get(ctx, types.NamespacedName{
107 Name: deviceBinding.Name,
108 Namespace: deviceBinding.Namespace,
109 }, devb); err != nil {
110 c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get device binding: " + err.Error()})
111 return
112 }
113
114 if err := bootstrap(ctx, k8sClient, c, devb); err != nil {
115 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
116 return
117 }
118 }
119
120 func register(ctx context.Context, k8sClient client.Client, c *gin.Context, req DeviceConnectRequest, deviceBinding *api.DeviceBinding) error {
121
122 device, applicationSubjects, extApps, err := getDeviceAndAppSubjects(ctx, k8sClient, c, req)
123 if err != nil {
124 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
125 return err
126 }
127
128
129 err = updateDeviceBinding(ctx, k8sClient, device, deviceBinding.GetName(), deviceBinding.GetNamespace(), applicationSubjects)
130 if err != nil {
131 c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update device binding: " + err.Error()})
132 return err
133 }
134
135
136 storeID := ""
137 if storeID, err = config.GetStoreID(ctx, k8sClient); err != nil {
138 c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to retrieve edge-info: " + err.Error()})
139 return err
140 }
141
142 cert := createCert(device, storeID)
143 if err := k8sClient.Create(ctx, cert); err != nil {
144 c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create client certificate: " + err.Error()})
145 return err
146 }
147
148
149 for _, extApp := range extApps {
150 client := createClient(device, extApp, deviceBinding)
151
152 client.Spec.SecretName = device.GetName() + "-" + extApp.GetName() + "-secret"
153 if err := k8sClient.Create(ctx, client); err != nil && !k8serrors.IsAlreadyExists(err) {
154 c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create edgeID client: " + err.Error()})
155 return err
156 }
157
158 _, err = waitForSecret(ctx, k8sClient, device.GetNamespace(), client.Spec.SecretName)
159 if err != nil {
160 c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to wait for client secret: " + err.Error()})
161 return err
162 }
163 }
164
165 return nil
166 }
167
168 func bootstrap(ctx context.Context, k8sClient client.Client, c *gin.Context, devb *api.DeviceBinding) error {
169 applicationResponse, err := getApplicationResponse(ctx, k8sClient, devb)
170 if err != nil {
171 return fmt.Errorf("failed to find EdgeID client secret: %w", err)
172 }
173
174 vip, err := config.GetVIP(ctx, k8sClient)
175 if err != nil {
176 return fmt.Errorf("failed to retrieve VIP address: %w", err)
177 }
178
179 secretName := devb.Spec.Device.Name + "-cert"
180 deviceAuthSecret, err := waitForSecret(ctx, k8sClient, "emissary", secretName)
181 if err != nil {
182 return fmt.Errorf("failed to wait for device auth secret, %w", err)
183 }
184
185 resp := DeviceConnectResponse{
186 Applications: applicationResponse,
187 HostMapping: HostMapping{
188 Host: config.Host,
189 VIP: vip,
190 },
191 DeviceAuth: DeviceAuth{
192 Crt: deviceAuthSecret.Data["tls.crt"],
193 Key: deviceAuthSecret.Data["tls.key"],
194 },
195 }
196 c.JSON(http.StatusOK, resp)
197 return nil
198 }
199
200 func updateDeviceBinding(ctx context.Context, k8sClient client.Client, device *api.ExternalDevice,
201 deviceBindingName string, namespace string, applicationSubjects []api.ApplicationSubject) error {
202
203 deviceBinding := &api.DeviceBinding{}
204 if err := k8sClient.Get(ctx, types.NamespacedName{
205 Name: deviceBindingName,
206 Namespace: namespace,
207 }, deviceBinding); err != nil {
208 return fmt.Errorf("failed to get device binding: %w", err)
209 }
210
211
212 deviceBinding.ObjectMeta.OwnerReferences = []metav1.OwnerReference{
213 {
214 APIVersion: api.GroupVersion.String(),
215 Kind: "ExternalDevice",
216 Name: device.GetName(),
217 UID: device.GetUID(),
218 },
219 }
220
221
222 deviceBinding.Spec.Device = api.DeviceSubject{
223 APIGroup: api.GroupVersion.Group,
224 Kind: "ExternalDevice",
225 Name: device.GetName(),
226 }
227 deviceBinding.Spec.Applications = applicationSubjects
228
229
230 if err := k8sClient.Update(ctx, deviceBinding); err != nil {
231 return fmt.Errorf("failed to update device binding: %w", err)
232 }
233
234 deviceBinding.Status.CodeUsed = true
235 if err := k8sClient.Status().Update(ctx, deviceBinding); err != nil {
236 return fmt.Errorf("failed to update device status: %w", err)
237 }
238
239 return nil
240 }
241
242
243
244 func createDevice(ctx context.Context, k8sClient client.Client, req DeviceConnectRequest) (*api.ExternalDevice, error) {
245
246 device := &api.ExternalDevice{}
247 err := k8sClient.Get(ctx, types.NamespacedName{
248 Name: req.Device.Name,
249 Namespace: config.Namespace,
250 }, device)
251
252
253 if err == nil {
254 if err := deleteDevice(ctx, k8sClient, device); err != nil {
255 return nil, err
256 }
257 } else if !k8serrors.IsNotFound(err) {
258
259 return nil, err
260 }
261
262
263 device = &api.ExternalDevice{
264 ObjectMeta: metav1.ObjectMeta{
265 Name: req.Device.Name,
266 Namespace: config.Namespace,
267 },
268 Spec: req.Device,
269 }
270 if err := k8sClient.Create(ctx, device); err != nil {
271 return nil, fmt.Errorf("timeout waiting for client secret to be created")
272 }
273
274 return device, nil
275 }
276
277 func deleteDevice(ctx context.Context, k8sClient client.Client, device *api.ExternalDevice) error {
278
279 err := k8sClient.Delete(ctx, device)
280 if err != nil {
281 return err
282 }
283 err = k8sClient.Delete(ctx, &cmApi.Certificate{
284 ObjectMeta: metav1.ObjectMeta{
285 Name: device.GetName(),
286 Namespace: "emissary",
287 },
288 })
289 if err != nil && !k8serrors.IsNotFound(err) {
290 return err
291 }
292 err = k8sClient.Delete(ctx, &corev1.Secret{
293 ObjectMeta: metav1.ObjectMeta{
294 Name: device.GetName() + "-cert",
295 Namespace: "emissary",
296 },
297 })
298 if err != nil && !k8serrors.IsNotFound(err) {
299 return err
300 }
301 return nil
302 }
303
304 func checkRequest(c *gin.Context) (DeviceConnectRequest, error) {
305 var req DeviceConnectRequest
306 if err := c.ShouldBindJSON(&req); err != nil {
307 return req, fmt.Errorf("failed to bind JSON: %w", err)
308 }
309
310
311 if req.Device.Name == "" {
312 return req, fmt.Errorf("Device Name is required")
313 }
314
315
316 if req.Device.SN == "" {
317 return req, fmt.Errorf("Serial Number is required")
318 }
319
320 return req, nil
321 }
322
323
324 func getAppSubjectsAndExternalApps(ctx context.Context, k8sClient client.Client, c *gin.Context, req DeviceConnectRequest) ([]api.ApplicationSubject, []*api.ExternalApplication, error) {
325 applicationSubjects := []api.ApplicationSubject{}
326 extApps := []*api.ExternalApplication{}
327 for _, appID := range req.ApplicationIDs {
328 extApp, err := getExternalApplicationByID(ctx, c, k8sClient, appID)
329 if err != nil {
330 return nil, nil, fmt.Errorf("failed to get External Application: %w", err)
331 }
332 extApps = append(extApps, extApp)
333 applicationSubjects = append(applicationSubjects,
334 api.ApplicationSubject{
335 APIGroup: api.GroupVersion.Group,
336 Kind: "ExternalApplication",
337 Name: extApp.GetName(),
338 ID: extApp.Spec.ID,
339 },
340 )
341 }
342 return applicationSubjects, extApps, nil
343 }
344
345 func createCert(device *api.ExternalDevice, storeID string) *cmApi.Certificate {
346 return &cmApi.Certificate{
347 ObjectMeta: metav1.ObjectMeta{
348 Name: device.GetName(),
349 Namespace: "emissary",
350 },
351 Spec: cmApi.CertificateSpec{
352 SecretName: device.GetName() + "-cert",
353 IssuerRef: cmMeta.ObjectReference{
354 Name: "device-registrar-ca-issuer",
355 Kind: "Issuer",
356 Group: "cert-manager.io",
357 },
358 Usages: []cmApi.KeyUsage{
359 cmApi.UsageClientAuth,
360 },
361 Duration: &metav1.Duration{Duration: 8760 * time.Hour},
362 RenewBefore: &metav1.Duration{Duration: 2184 * time.Hour},
363 DNSNames: []string{"edge.store.ncr.corp"},
364 Subject: &cmApi.X509Subject{
365 OrganizationalUnits: []string{
366 "urn:deviceName:" + device.GetName(),
367 "urn:storeID:" + storeID,
368 "urn:SN:" + device.Spec.SN,
369 },
370 },
371 },
372 }
373 }
374
375 func createClient(device *api.ExternalDevice, extApp *api.ExternalApplication, deviceBinding *api.DeviceBinding) *clientApi.Client {
376 clientName := device.GetName() + "-" + extApp.GetName() + "-client"
377 return &clientApi.Client{
378 ObjectMeta: metav1.ObjectMeta{
379 GenerateName: clientName + "-",
380 Namespace: device.GetNamespace(),
381 OwnerReferences: []metav1.OwnerReference{
382 {
383 APIVersion: api.GroupVersion.String(),
384 Kind: "DeviceBinding",
385 Name: deviceBinding.GetName(),
386 UID: deviceBinding.GetUID(),
387 },
388 },
389 },
390 Spec: extApp.Spec.ClientTemplate.Spec,
391 }
392 }
393
394
395 func waitForSecret(ctx context.Context, k8sClient client.Client, ns string, secretName string) (corev1.Secret, error) {
396 err := error(nil)
397 secretFound := false
398 timeout := time.After(30 * time.Second)
399
400 secret := &corev1.Secret{}
401 for !secretFound {
402 select {
403 case <-timeout:
404 return corev1.Secret{}, fmt.Errorf("timeout waiting for client secret to be created %w", err)
405 default:
406 err := k8sClient.Get(ctx, types.NamespacedName{
407 Name: secretName,
408 Namespace: ns,
409 }, secret)
410 if err == nil {
411 secretFound = true
412 break
413 }
414 if !k8serrors.IsNotFound(err) {
415 return corev1.Secret{}, fmt.Errorf("timeout waiting for client secret to be created %w", err)
416 }
417 time.Sleep(1 * time.Second)
418 }
419 }
420
421 return *secret, nil
422 }
423
424 func getApplicationResponse(ctx context.Context, k8sClient client.Client, devb *api.DeviceBinding) ([]ApplicationResponse, error) {
425 applicationResponse := []ApplicationResponse{}
426 for _, app := range devb.Spec.Applications {
427 clientSecretName := devb.Spec.Device.Name + "-" + app.Name + "-secret"
428 deviceSecret := &corev1.Secret{}
429 err := k8sClient.Get(ctx, types.NamespacedName{Name: clientSecretName, Namespace: devb.Namespace}, deviceSecret)
430 if err != nil {
431 return nil, err
432 }
433 applicationResponse = append(applicationResponse, ApplicationResponse{
434 ID: app.ID,
435 Name: app.Name,
436 Config: EdgeIDClient{
437 ClientID: string(deviceSecret.Data["client_id"]),
438 ClientSecret: string(deviceSecret.Data["client_secret"]),
439 },
440 })
441 }
442 return applicationResponse, nil
443 }
444
445 func getDeviceAndAppSubjects(ctx context.Context, k8sClient client.Client, c *gin.Context, req DeviceConnectRequest) (*api.ExternalDevice, []api.ApplicationSubject, []*api.ExternalApplication, error) {
446 device, err := createDevice(ctx, k8sClient, req)
447 if err != nil {
448 return nil, nil, nil, fmt.Errorf("failed to create external device: %w", err)
449 }
450
451 applicationSubjects, extApps, err := getAppSubjectsAndExternalApps(ctx, k8sClient, c, req)
452 if err != nil {
453 return nil, nil, nil, fmt.Errorf("failed to get External Application: %w", err)
454 }
455
456 return device, applicationSubjects, extApps, nil
457 }
458
459
460 func retrieveDeviceRegistration(ctx context.Context, k8sClient client.Client, c *gin.Context) (*api.DeviceBinding, error) {
461 activationCode := c.Param("activationCode")
462 if activationCode == "" {
463 return nil, errActivationCodeNotFound
464 }
465
466
467 deviceBindingList := &api.DeviceBindingList{}
468 err := k8sClient.List(ctx, deviceBindingList)
469 if err != nil {
470 return nil, fmt.Errorf("failed to list device bindings: %w", err)
471 }
472
473 for i := range deviceBindingList.Items {
474 device := deviceBindingList.Items[i]
475 if device.Status.ActivationCode == activationCode {
476 if device.Status.CodeUsed {
477 return nil, errActivationCodeUsed
478 }
479 timestamp := device.Status.Timestamp.Time
480 if time.Since(timestamp) > time.Hour {
481 return nil, errActivationCodeExpired
482 }
483 return &device, err
484 }
485 }
486 return nil, errDeviceBindingNotFound
487 }
488
489
490
491
492
493
494
495 type DeviceConnectRequestParams struct {
496
497 Body DeviceConnectRequest
498 }
499
View as plain text