blob: d2c18c38fa337527689d3df21757b9a5d275396f [file] [log] [blame]
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +02001// Copyright 2020 The Monogon Project Authors.
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
17package api
18
19import (
20 "context"
21 "crypto/rand"
22 "encoding/hex"
23 "fmt"
24 schema "git.monogon.dev/source/nexantic.git/core/generated/api"
Leopold Schabel68c58752019-11-14 21:00:59 +010025 "git.monogon.dev/source/nexantic.git/core/internal/common/grpc"
Serge Bazanski7ba31522020-02-03 16:08:19 +010026 "google.golang.org/grpc/codes"
27 "google.golang.org/grpc/status"
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +020028
29 "go.uber.org/zap"
30)
31
32var (
Serge Bazanski7ba31522020-02-03 16:08:19 +010033 ErrAttestationFailed = status.Error(codes.PermissionDenied, "attestation failed")
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +020034)
35
36func (s *Server) AddNode(ctx context.Context, req *schema.AddNodeRequest) (*schema.AddNodeResponse, error) {
37 // Setup API client
Leopold Schabel68c58752019-11-14 21:00:59 +010038 c, err := grpc.NewSmalltownAPIClient(fmt.Sprintf("%s:%d", req.Addr, s.config.Port))
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +020039 if err != nil {
40 return nil, err
41 }
42
43 // Check attestation
44 nonce := make([]byte, 20)
45 _, err = rand.Read(nonce)
46 if err != nil {
Serge Bazanski7ba31522020-02-03 16:08:19 +010047 s.Logger.Error("Nonce generation failed", zap.Error(err))
48 return nil, status.Error(codes.Unavailable, "nonce generation failed")
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +020049 }
50 hexNonce := hex.EncodeToString(nonce)
51
52 aRes, err := c.Setup.Attest(ctx, &schema.AttestRequest{
53 Challenge: hexNonce,
54 })
55 if err != nil {
Serge Bazanski7ba31522020-02-03 16:08:19 +010056 s := status.Convert(err)
57 return nil, status.Errorf(s.Code(), "attestation failed: %v", s.Message())
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +020058 }
59
60 //TODO(hendrik): Verify response
61 if aRes.Response != hexNonce {
62 return nil, ErrAttestationFailed
63 }
64
Leopold Schabel68c58752019-11-14 21:00:59 +010065 consensusCerts, err := s.consensusService.IssueCertificate(req.Addr)
Lorenz Bruna4ea9d02019-10-31 11:40:30 +010066 if err != nil {
Serge Bazanski7ba31522020-02-03 16:08:19 +010067 // Errors from IssueCertificate are always treated as internal
68 s.Logger.Error("Node certificate issuance failed", zap.String("addr", req.Addr), zap.Error(err))
69 return nil, status.Error(codes.Internal, "could not issue node certificate")
Lorenz Bruna4ea9d02019-10-31 11:40:30 +010070 }
71
Leopold Schabel68c58752019-11-14 21:00:59 +010072 // TODO(leo): fetch remote hostname rather than using the addr
73 name := req.Addr
74
75 // Add new node to local etcd cluster.
76 memberID, err := s.consensusService.AddMember(ctx, name, fmt.Sprintf("https://%s:%d", req.Addr, s.config.Port))
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +020077 if err != nil {
Serge Bazanski7ba31522020-02-03 16:08:19 +010078 return nil, status.Errorf(codes.Unavailable, "failed to add node to etcd cluster: %v", err)
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +020079 }
80
Leopold Schabel68c58752019-11-14 21:00:59 +010081 s.Logger.Info("Added new node to consensus cluster; sending cluster join request to node",
82 zap.String("addr", req.Addr), zap.Uint16("port", s.config.Port))
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +020083
Leopold Schabel68c58752019-11-14 21:00:59 +010084 // Send JoinCluster request to new node to make it join.
85 _, err = c.Setup.JoinCluster(ctx, &schema.JoinClusterRequest{
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +020086 InitialCluster: s.consensusService.GetInitialClusterString(),
Leopold Schabel68c58752019-11-14 21:00:59 +010087 ProvisioningToken: req.ProvisioningToken,
Lorenz Bruna4ea9d02019-10-31 11:40:30 +010088 Certs: consensusCerts,
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +020089 })
90 if err != nil {
Leopold Schabel68c58752019-11-14 21:00:59 +010091 errRevoke := s.consensusService.RevokeCertificate(req.Addr)
92 if errRevoke != nil {
93 s.Logger.Error("Failed to revoke a certificate after rollback - potential security risk", zap.Error(errRevoke))
Lorenz Bruna4ea9d02019-10-31 11:40:30 +010094 }
Leopold Schabel68c58752019-11-14 21:00:59 +010095 // Revert etcd add member - might fail if consensus cannot be established.
96 errRemove := s.consensusService.RemoveMember(ctx, memberID)
97 if errRemove != nil || errRevoke != nil {
98 return nil, fmt.Errorf("rollback failed after failed provisioning; err=%v; err_rb=%v; err_revoke=%v", err, errRemove, errRevoke)
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +020099 }
Serge Bazanski7ba31522020-02-03 16:08:19 +0100100 return nil, status.Errorf(codes.Unavailable, "failed to join etcd cluster with node: %v", err)
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +0200101 }
102 s.Logger.Info("Fully provisioned new node",
Leopold Schabel68c58752019-11-14 21:00:59 +0100103 zap.String("host", req.Addr),
104 zap.Uint16("apiPort", s.config.Port),
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +0200105 zap.Uint64("member_id", memberID))
106
107 return &schema.AddNodeResponse{}, nil
108}
109
Serge Bazanski71049af2020-02-03 16:05:52 +0100110func (s *Server) RemoveNode(context.Context, *schema.RemoveNodeRequest) (*schema.RemoveNodeResponse, error) {
Serge Bazanski7ba31522020-02-03 16:08:19 +0100111 return nil, status.Error(codes.Unimplemented, "unimplemented")
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +0200112}
113
Leopold Schabel68c58752019-11-14 21:00:59 +0100114func (s *Server) ListNodes(context.Context, *schema.ListNodesRequest) (*schema.ListNodesResponse, error) {
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +0200115 nodes := s.consensusService.GetNodes()
116 resNodes := make([]*schema.Node, len(nodes))
117
118 for i, node := range nodes {
119 resNodes[i] = &schema.Node{
120 Id: node.ID,
121 Name: node.Name,
122 Address: node.Address,
123 Synced: node.Synced,
124 }
125 }
126
Leopold Schabel68c58752019-11-14 21:00:59 +0100127 return &schema.ListNodesResponse{
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +0200128 Nodes: resNodes,
129 }, nil
130}