diff --git a/client/cmd/testutil.go b/client/cmd/testutil.go index 988ef8cc0..678072f0b 100644 --- a/client/cmd/testutil.go +++ b/client/cmd/testutil.go @@ -81,7 +81,7 @@ func startManagement(t *testing.T, config *mgmt.Config) (*grpc.Server, net.Liste t.Fatal(err) } turnManager := mgmt.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig) - mgmtServer, err := mgmt.NewServer(config, accountManager, peersUpdateManager, turnManager, nil) + mgmtServer, err := mgmt.NewServer(config, accountManager, peersUpdateManager, turnManager, nil, nil) if err != nil { t.Fatal(err) } diff --git a/client/internal/engine_test.go b/client/internal/engine_test.go index 60c07c0c9..d1c46181c 100644 --- a/client/internal/engine_test.go +++ b/client/internal/engine_test.go @@ -1054,7 +1054,7 @@ func startManagement(dataDir string) (*grpc.Server, string, error) { return nil, "", err } turnManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig) - mgmtServer, err := server.NewServer(config, accountManager, peersUpdateManager, turnManager, nil) + mgmtServer, err := server.NewServer(config, accountManager, peersUpdateManager, turnManager, nil, nil) if err != nil { return nil, "", err } diff --git a/management/client/client_test.go b/management/client/client_test.go index d3d99dc85..deef57329 100644 --- a/management/client/client_test.go +++ b/management/client/client_test.go @@ -66,7 +66,7 @@ func startManagement(t *testing.T) (*grpc.Server, net.Listener) { t.Fatal(err) } turnManager := mgmt.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig) - mgmtServer, err := mgmt.NewServer(config, accountManager, peersUpdateManager, turnManager, nil) + mgmtServer, err := mgmt.NewServer(config, accountManager, peersUpdateManager, turnManager, nil, nil) if err != nil { t.Fatal(err) } diff --git a/management/cmd/management.go b/management/cmd/management.go index a4d67fc0b..5c3816715 100644 --- a/management/cmd/management.go +++ b/management/cmd/management.go @@ -198,8 +198,11 @@ var ( return fmt.Errorf("failed creating HTTP API handler: %v", err) } + ephemeralManager := server.NewEphemeralManager(store, accountManager) + ephemeralManager.LoadInitialPeers() + gRPCAPIHandler := grpc.NewServer(gRPCOpts...) - srv, err := server.NewServer(config, accountManager, peersUpdateManager, turnManager, appMetrics) + srv, err := server.NewServer(config, accountManager, peersUpdateManager, turnManager, appMetrics, ephemeralManager) if err != nil { return fmt.Errorf("failed creating gRPC API handler: %v", err) } @@ -268,6 +271,7 @@ var ( SetupCloseHandler() <-stopCh + ephemeralManager.Stop() _ = appMetrics.Close() _ = listener.Close() if certManager != nil { diff --git a/management/server/account.go b/management/server/account.go index d9b73f713..26aeed3c5 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -49,7 +49,7 @@ func cacheEntryExpiration() time.Duration { type AccountManager interface { GetOrCreateAccountByUser(userId, domain string) (*Account, error) CreateSetupKey(accountID string, keyName string, keyType SetupKeyType, expiresIn time.Duration, - autoGroups []string, usageLimit int, userID string) (*SetupKey, error) + autoGroups []string, usageLimit int, userID string, ephemeral bool) (*SetupKey, error) SaveSetupKey(accountID string, key *SetupKey, userID string) (*SetupKey, error) CreateUser(accountID, initiatorUserID string, key *UserInfo) (*UserInfo, error) DeleteUser(accountID, initiatorUserID string, targetUserID string) error diff --git a/management/server/account_test.go b/management/server/account_test.go index 29af8514a..6002b7a3a 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -12,6 +12,7 @@ import ( "time" "github.com/golang-jwt/jwt" + nbdns "github.com/netbirdio/netbird/dns" "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/route" @@ -782,7 +783,7 @@ func TestAccountManager_AddPeer(t *testing.T) { serial := account.Network.CurrentSerial() // should be 0 - setupKey, err := manager.CreateSetupKey(account.Id, "test-key", SetupKeyReusable, time.Hour, nil, 999, userID) + setupKey, err := manager.CreateSetupKey(account.Id, "test-key", SetupKeyReusable, time.Hour, nil, 999, userID, false) if err != nil { return } @@ -929,7 +930,7 @@ func TestAccountManager_NetworkUpdates(t *testing.T) { t.Fatal(err) } - setupKey, err := manager.CreateSetupKey(account.Id, "test-key", SetupKeyReusable, time.Hour, nil, 999, userID) + setupKey, err := manager.CreateSetupKey(account.Id, "test-key", SetupKeyReusable, time.Hour, nil, 999, userID, false) if err != nil { return } @@ -1113,7 +1114,7 @@ func TestAccountManager_DeletePeer(t *testing.T) { t.Fatal(err) } - setupKey, err := manager.CreateSetupKey(account.Id, "test-key", SetupKeyReusable, time.Hour, nil, 999, userID) + setupKey, err := manager.CreateSetupKey(account.Id, "test-key", SetupKeyReusable, time.Hour, nil, 999, userID, false) if err != nil { return } diff --git a/management/server/activity/event.go b/management/server/activity/event.go index 668449176..17ec4a0b0 100644 --- a/management/server/activity/event.go +++ b/management/server/activity/event.go @@ -4,6 +4,10 @@ import ( "time" ) +const ( + SystemInitiator = "sys" +) + // Event represents a network/system activity event. type Event struct { // Timestamp of the event diff --git a/management/server/ephemeral.go b/management/server/ephemeral.go new file mode 100644 index 000000000..a7b423983 --- /dev/null +++ b/management/server/ephemeral.go @@ -0,0 +1,224 @@ +package server + +import ( + "sync" + "time" + + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/management/server/activity" +) + +const ( + ephemeralLifeTime = 10 * time.Minute +) + +var ( + timeNow = time.Now +) + +type ephemeralPeer struct { + id string + account *Account + deadline time.Time + next *ephemeralPeer +} + +// todo: consider to remove peer from ephemeral list when the peer has been deleted via API. If we do not do it +// in worst case we will get invalid error message in this manager. + +// EphemeralManager keep a list of ephemeral peers. After ephemeralLifeTime inactivity the peer will be deleted +// automatically. Inactivity means the peer disconnected from the Management server. +type EphemeralManager struct { + store Store + accountManager AccountManager + + headPeer *ephemeralPeer + tailPeer *ephemeralPeer + peersLock sync.Mutex + timer *time.Timer +} + +// NewEphemeralManager instantiate new EphemeralManager +func NewEphemeralManager(store Store, accountManager AccountManager) *EphemeralManager { + return &EphemeralManager{ + store: store, + accountManager: accountManager, + } +} + +// LoadInitialPeers load from the database the ephemeral type of peers and schedule a cleanup procedure to the head +// of the linked list (to the most deprecated peer). At the end of cleanup it schedules the next cleanup to the new +// head. +func (e *EphemeralManager) LoadInitialPeers() { + e.peersLock.Lock() + defer e.peersLock.Unlock() + + e.loadEphemeralPeers() + if e.headPeer != nil { + e.timer = time.AfterFunc(ephemeralLifeTime, e.cleanup) + } +} + +// Stop timer +func (e *EphemeralManager) Stop() { + e.peersLock.Lock() + defer e.peersLock.Unlock() + + if e.timer != nil { + e.timer.Stop() + } +} + +// OnPeerConnected remove the peer from the linked list of ephemeral peers. Because it has been called when the peer +// is active the manager will not delete it while it is active. +func (e *EphemeralManager) OnPeerConnected(peer *Peer) { + if !peer.Ephemeral { + return + } + + log.Tracef("remove peer from ephemeral list: %s", peer.ID) + + e.peersLock.Lock() + defer e.peersLock.Unlock() + + e.removePeer(peer.ID) + + // stop the unnecessary timer + if e.headPeer == nil && e.timer != nil { + e.timer.Stop() + e.timer = nil + } +} + +// OnPeerDisconnected add the peer to the linked list of ephemeral peers. Because of the peer +// is inactive it will be deleted after the ephemeralLifeTime period. +func (e *EphemeralManager) OnPeerDisconnected(peer *Peer) { + if !peer.Ephemeral { + return + } + + log.Tracef("add peer to ephemeral list: %s", peer.ID) + + a, err := e.store.GetAccountByPeerID(peer.ID) + if err != nil { + log.Errorf("failed to add peer to ephemeral list: %s", err) + return + } + + e.peersLock.Lock() + defer e.peersLock.Unlock() + + if e.isPeerOnList(peer.ID) { + return + } + + e.addPeer(peer.ID, a, newDeadLine()) + if e.timer == nil { + e.timer = time.AfterFunc(e.headPeer.deadline.Sub(timeNow()), e.cleanup) + } +} + +func (e *EphemeralManager) loadEphemeralPeers() { + accounts := e.store.GetAllAccounts() + t := newDeadLine() + count := 0 + for _, a := range accounts { + for id, p := range a.Peers { + if p.Ephemeral { + count++ + e.addPeer(id, a, t) + } + } + } + log.Debugf("loaded ephemeral peer(s): %d", count) +} + +func (e *EphemeralManager) cleanup() { + log.Tracef("on ephemeral cleanup") + deletePeers := make(map[string]*ephemeralPeer) + + e.peersLock.Lock() + now := timeNow() + for p := e.headPeer; p != nil; p = p.next { + if now.Before(p.deadline) { + break + } + + deletePeers[p.id] = p + e.headPeer = p.next + if p.next == nil { + e.tailPeer = nil + } + } + + if e.headPeer != nil { + e.timer = time.AfterFunc(e.headPeer.deadline.Sub(timeNow()), e.cleanup) + } else { + e.timer = nil + } + + e.peersLock.Unlock() + + for id, p := range deletePeers { + log.Debugf("delete ephemeral peer: %s", id) + _, err := e.accountManager.DeletePeer(p.account.Id, id, activity.SystemInitiator) + if err != nil { + log.Tracef("failed to delete ephemeral peer: %s", err) + } + } +} + +func (e *EphemeralManager) addPeer(id string, account *Account, deadline time.Time) { + ep := &ephemeralPeer{ + id: id, + account: account, + deadline: deadline, + } + + if e.headPeer == nil { + e.headPeer = ep + } + if e.tailPeer != nil { + e.tailPeer.next = ep + } + e.tailPeer = ep +} + +func (e *EphemeralManager) removePeer(id string) { + if e.headPeer == nil { + return + } + + if e.headPeer.id == id { + e.headPeer = e.headPeer.next + if e.tailPeer.id == id { + e.tailPeer = nil + } + return + } + + for p := e.headPeer; p.next != nil; p = p.next { + if p.next.id == id { + // if we remove the last element from the chain then set the last-1 as tail + if e.tailPeer.id == id { + e.tailPeer = p + } + p.next = p.next.next + return + } + } +} + +func (e *EphemeralManager) isPeerOnList(id string) bool { + for p := e.headPeer; p != nil; p = p.next { + if p.id == id { + return true + } + } + return false +} + +func newDeadLine() time.Time { + return timeNow().Add(ephemeralLifeTime) +} diff --git a/management/server/ephemeral_test.go b/management/server/ephemeral_test.go new file mode 100644 index 000000000..554d2a028 --- /dev/null +++ b/management/server/ephemeral_test.go @@ -0,0 +1,142 @@ +package server + +import ( + "fmt" + "testing" + "time" +) + +type MockStore struct { + Store + account *Account +} + +func (s *MockStore) GetAllAccounts() []*Account { + return []*Account{s.account} +} + +func (s *MockStore) GetAccountByPeerID(peerId string) (*Account, error) { + _, ok := s.account.Peers[peerId] + if ok { + return s.account, nil + } + + return nil, fmt.Errorf("account not found") +} + +type MocAccountManager struct { + AccountManager + store *MockStore +} + +func (a MocAccountManager) DeletePeer(accountID, peerID, userID string) (*Peer, error) { + delete(a.store.account.Peers, peerID) + return nil, nil +} + +func TestNewManager(t *testing.T) { + startTime := time.Now() + timeNow = func() time.Time { + return startTime + } + + store := &MockStore{} + am := MocAccountManager{ + store: store, + } + + numberOfPeers := 5 + numberOfEphemeralPeers := 3 + seedPeers(store, numberOfPeers, numberOfEphemeralPeers) + + mgr := NewEphemeralManager(store, am) + mgr.loadEphemeralPeers() + startTime = startTime.Add(ephemeralLifeTime + 1) + mgr.cleanup() + + if len(store.account.Peers) != numberOfPeers { + t.Errorf("failed to cleanup ephemeral peers, expected: %d, result: %d", numberOfPeers, len(store.account.Peers)) + } +} + +func TestNewManagerPeerConnected(t *testing.T) { + startTime := time.Now() + timeNow = func() time.Time { + return startTime + } + + store := &MockStore{} + am := MocAccountManager{ + store: store, + } + + numberOfPeers := 5 + numberOfEphemeralPeers := 3 + seedPeers(store, numberOfPeers, numberOfEphemeralPeers) + + mgr := NewEphemeralManager(store, am) + mgr.loadEphemeralPeers() + mgr.OnPeerConnected(store.account.Peers["ephemeral_peer_0"]) + + startTime = startTime.Add(ephemeralLifeTime + 1) + mgr.cleanup() + + expected := numberOfPeers + 1 + if len(store.account.Peers) != expected { + t.Errorf("failed to cleanup ephemeral peers, expected: %d, result: %d", expected, len(store.account.Peers)) + } +} + +func TestNewManagerPeerDisconnected(t *testing.T) { + startTime := time.Now() + timeNow = func() time.Time { + return startTime + } + + store := &MockStore{} + am := MocAccountManager{ + store: store, + } + + numberOfPeers := 5 + numberOfEphemeralPeers := 3 + seedPeers(store, numberOfPeers, numberOfEphemeralPeers) + + mgr := NewEphemeralManager(store, am) + mgr.loadEphemeralPeers() + for _, v := range store.account.Peers { + mgr.OnPeerConnected(v) + + } + mgr.OnPeerDisconnected(store.account.Peers["ephemeral_peer_0"]) + + startTime = startTime.Add(ephemeralLifeTime + 1) + mgr.cleanup() + + expected := numberOfPeers + numberOfEphemeralPeers - 1 + if len(store.account.Peers) != expected { + t.Errorf("failed to cleanup ephemeral peers, expected: %d, result: %d", expected, len(store.account.Peers)) + } +} + +func seedPeers(store *MockStore, numberOfPeers int, numberOfEphemeralPeers int) { + store.account = newAccountWithId("my account", "", "") + + for i := 0; i < numberOfPeers; i++ { + peerId := fmt.Sprintf("peer_%d", i) + p := &Peer{ + ID: peerId, + Ephemeral: false, + } + store.account.Peers[p.ID] = p + } + + for i := 0; i < numberOfEphemeralPeers; i++ { + peerId := fmt.Sprintf("ephemeral_peer_%d", i) + p := &Peer{ + ID: peerId, + Ephemeral: true, + } + store.account.Peers[p.ID] = p + } +} diff --git a/management/server/grpcserver.go b/management/server/grpcserver.go index 94cb1de9d..32b553f9b 100644 --- a/management/server/grpcserver.go +++ b/management/server/grpcserver.go @@ -7,11 +7,6 @@ import ( "time" pb "github.com/golang/protobuf/proto" // nolint - - "github.com/netbirdio/netbird/management/server/telemetry" - - "github.com/netbirdio/netbird/management/server/jwtclaims" - "github.com/golang/protobuf/ptypes/timestamp" log "github.com/sirupsen/logrus" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" @@ -21,7 +16,9 @@ import ( "github.com/netbirdio/netbird/encryption" "github.com/netbirdio/netbird/management/proto" + "github.com/netbirdio/netbird/management/server/jwtclaims" internalStatus "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/management/server/telemetry" ) // GRPCServer an instance of a Management gRPC API server @@ -35,12 +32,11 @@ type GRPCServer struct { jwtValidator *jwtclaims.JWTValidator jwtClaimsExtractor *jwtclaims.ClaimsExtractor appMetrics telemetry.AppMetrics + ephemeralManager *EphemeralManager } // NewServer creates a new Management server -func NewServer(config *Config, accountManager AccountManager, peersUpdateManager *PeersUpdateManager, - turnCredentialsManager TURNCredentialsManager, appMetrics telemetry.AppMetrics, -) (*GRPCServer, error) { +func NewServer(config *Config, accountManager AccountManager, peersUpdateManager *PeersUpdateManager, turnCredentialsManager TURNCredentialsManager, appMetrics telemetry.AppMetrics, ephemeralManager *EphemeralManager) (*GRPCServer, error) { key, err := wgtypes.GeneratePrivateKey() if err != nil { return nil, err @@ -92,6 +88,7 @@ func NewServer(config *Config, accountManager AccountManager, peersUpdateManager jwtValidator: jwtValidator, jwtClaimsExtractor: jwtClaimsExtractor, appMetrics: appMetrics, + ephemeralManager: ephemeralManager, }, nil } @@ -141,6 +138,9 @@ func (s *GRPCServer) Sync(req *proto.EncryptedMessage, srv proto.ManagementServi } updates := s.peersUpdateManager.CreateChannel(peer.ID) + + s.ephemeralManager.OnPeerConnected(peer) + err = s.accountManager.MarkPeerConnected(peerKey.String(), true) if err != nil { log.Warnf("failed marking peer as connected %s %v", peerKey, err) @@ -168,6 +168,7 @@ func (s *GRPCServer) Sync(req *proto.EncryptedMessage, srv proto.ManagementServi encryptedResp, err := encryption.EncryptMessage(peerKey, s.wgKey, update.Update) if err != nil { + s.cancelPeerRoutines(peer) return status.Errorf(codes.Internal, "failed processing update message") } @@ -176,6 +177,7 @@ func (s *GRPCServer) Sync(req *proto.EncryptedMessage, srv proto.ManagementServi Body: encryptedResp, }) if err != nil { + s.cancelPeerRoutines(peer) return status.Errorf(codes.Internal, "failed sending update message") } log.Debugf("sent an update to peer %s", peerKey.String()) @@ -193,6 +195,7 @@ func (s *GRPCServer) cancelPeerRoutines(peer *Peer) { s.peersUpdateManager.CloseChannel(peer.ID) s.turnCredentialsManager.CancelRefresh(peer.ID) _ = s.accountManager.MarkPeerConnected(peer.Key, false) + s.ephemeralManager.OnPeerDisconnected(peer) } func (s *GRPCServer) validateToken(jwtToken string) (string, error) { @@ -318,11 +321,17 @@ func (s *GRPCServer) Login(ctx context.Context, req *proto.EncryptedMessage) (*p UserID: userID, SetupKey: loginReq.GetSetupKey(), }) + if err != nil { log.Warnf("failed logging in peer %s", peerKey) return nil, mapError(err) } + // if the login request contains setup key then it is a registration request + if loginReq.GetSetupKey() != "" { + s.ephemeralManager.OnPeerDisconnected(peer) + } + // if peer has reached this point then it has logged in loginResp := &proto.LoginResponse{ WiretrusteeConfig: toWiretrusteeConfig(s.config, nil), diff --git a/management/server/http/api/openapi.yml b/management/server/http/api/openapi.yml index a09b9f6a6..06da0ede3 100644 --- a/management/server/http/api/openapi.yml +++ b/management/server/http/api/openapi.yml @@ -350,6 +350,10 @@ components: description: A number of times this key can be used. The value of 0 indicates the unlimited usage. type: integer example: 0 + ephemeral: + description: Indicate that the peer will be ephemeral or not + type: boolean + example: true required: - id - key @@ -364,6 +368,7 @@ components: - auto_groups - updated_at - usage_limit + - ephemeral SetupKeyRequest: type: object properties: @@ -395,6 +400,10 @@ components: description: A number of times this key can be used. The value of 0 indicates the unlimited usage. type: integer example: 0 + ephemeral: + description: Indicate that the peer will be ephemeral or not + type: boolean + example: true required: - name - type diff --git a/management/server/http/api/types.gen.go b/management/server/http/api/types.gen.go index 5b629cc0e..402aae635 100644 --- a/management/server/http/api/types.gen.go +++ b/management/server/http/api/types.gen.go @@ -689,6 +689,9 @@ type SetupKey struct { // AutoGroups List of group IDs to auto-assign to peers registered with this key AutoGroups []string `json:"auto_groups"` + // Ephemeral Indicate that the peer will be ephemeral or not + Ephemeral bool `json:"ephemeral"` + // Expires Setup Key expiration date Expires time.Time `json:"expires"` @@ -731,6 +734,9 @@ type SetupKeyRequest struct { // AutoGroups List of group IDs to auto-assign to peers registered with this key AutoGroups []string `json:"auto_groups"` + // Ephemeral Indicate that the peer will be ephemeral or not + Ephemeral *bool `json:"ephemeral,omitempty"` + // ExpiresIn Expiration time in seconds ExpiresIn int `json:"expires_in"` diff --git a/management/server/http/setupkeys_handler.go b/management/server/http/setupkeys_handler.go index 58a3c1091..392cebdbd 100644 --- a/management/server/http/setupkeys_handler.go +++ b/management/server/http/setupkeys_handler.go @@ -71,8 +71,12 @@ func (h *SetupKeysHandler) CreateSetupKey(w http.ResponseWriter, r *http.Request req.AutoGroups = []string{} } + var ephemeral bool + if req.Ephemeral != nil { + ephemeral = *req.Ephemeral + } setupKey, err := h.accountManager.CreateSetupKey(account.Id, req.Name, server.SetupKeyType(req.Type), expiresIn, - req.AutoGroups, req.UsageLimit, user.Id) + req.AutoGroups, req.UsageLimit, user.Id, ephemeral) if err != nil { util.WriteError(err, w) return diff --git a/management/server/http/setupkeys_handler_test.go b/management/server/http/setupkeys_handler_test.go index 4a5a9af62..afc9deb15 100644 --- a/management/server/http/setupkeys_handler_test.go +++ b/management/server/http/setupkeys_handler_test.go @@ -51,7 +51,7 @@ func initSetupKeysTestMetaData(defaultKey *server.SetupKey, newKey *server.Setup }, user, nil }, CreateSetupKeyFunc: func(_ string, keyName string, typ server.SetupKeyType, _ time.Duration, _ []string, - _ int, _ string, + _ int, _ string, _ bool, ) (*server.SetupKey, error) { if keyName == newKey.Name || typ != newKey.Type { return newKey, nil @@ -99,7 +99,7 @@ func TestSetupKeysHandlers(t *testing.T) { adminUser := server.NewAdminUser("test_user") newSetupKey := server.GenerateSetupKey(newSetupKeyName, server.SetupKeyReusable, 0, []string{"group-1"}, - server.SetupKeyUnlimitedUsage) + server.SetupKeyUnlimitedUsage, false) updatedDefaultSetupKey := defaultSetupKey.Copy() updatedDefaultSetupKey.AutoGroups = []string{"group-1"} updatedDefaultSetupKey.Name = updatedSetupKeyName diff --git a/management/server/management_proto_test.go b/management/server/management_proto_test.go index 6855c84bd..792d05187 100644 --- a/management/server/management_proto_test.go +++ b/management/server/management_proto_test.go @@ -422,7 +422,9 @@ func startManagement(t *testing.T, config *Config) (*grpc.Server, string, error) return nil, "", err } turnManager := NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig) - mgmtServer, err := NewServer(config, accountManager, peersUpdateManager, turnManager, nil) + + ephemeralMgr := NewEphemeralManager(store, accountManager) + mgmtServer, err := NewServer(config, accountManager, peersUpdateManager, turnManager, nil, ephemeralMgr) if err != nil { return nil, "", err } diff --git a/management/server/management_test.go b/management/server/management_test.go index 7af2535f8..6c93765f4 100644 --- a/management/server/management_test.go +++ b/management/server/management_test.go @@ -508,7 +508,7 @@ func startServer(config *server.Config) (*grpc.Server, net.Listener) { log.Fatalf("failed creating a manager: %v", err) } turnManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig) - mgmtServer, err := server.NewServer(config, accountManager, peersUpdateManager, turnManager, nil) + mgmtServer, err := server.NewServer(config, accountManager, peersUpdateManager, turnManager, nil, nil) Expect(err).NotTo(HaveOccurred()) mgmtProto.RegisterManagementServiceServer(s, mgmtServer) go func() { diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go index eb31d2a79..4bfa922c7 100644 --- a/management/server/mock_server/account_mock.go +++ b/management/server/mock_server/account_mock.go @@ -16,7 +16,7 @@ import ( type MockAccountManager struct { GetOrCreateAccountByUserFunc func(userId, domain string) (*server.Account, error) CreateSetupKeyFunc func(accountId string, keyName string, keyType server.SetupKeyType, - expiresIn time.Duration, autoGroups []string, usageLimit int, userID string) (*server.SetupKey, error) + expiresIn time.Duration, autoGroups []string, usageLimit int, userID string, ephemeral bool) (*server.SetupKey, error) GetSetupKeyFunc func(accountID, userID, keyID string) (*server.SetupKey, error) GetAccountByUserOrAccountIdFunc func(userId, accountId, domain string) (*server.Account, error) GetUserFunc func(claims jwtclaims.AuthorizationClaims) (*server.User, error) @@ -122,9 +122,10 @@ func (am *MockAccountManager) CreateSetupKey( autoGroups []string, usageLimit int, userID string, + ephemeral bool, ) (*server.SetupKey, error) { if am.CreateSetupKeyFunc != nil { - return am.CreateSetupKeyFunc(accountID, keyName, keyType, expiresIn, autoGroups, usageLimit, userID) + return am.CreateSetupKeyFunc(accountID, keyName, keyType, expiresIn, autoGroups, usageLimit, userID, ephemeral) } return nil, status.Errorf(codes.Unimplemented, "method CreateSetupKey is not implemented") } diff --git a/management/server/peer.go b/management/server/peer.go index 90377b1e8..f9631719f 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -99,6 +99,8 @@ type Peer struct { LoginExpirationEnabled bool // LastLogin the time when peer performed last login operation LastLogin time.Time + // Indicate ephemeral peer attribute + Ephemeral bool } // AddedWithSSOLogin indicates whether this peer has been added with an SSO login by a user. @@ -126,6 +128,7 @@ func (p *Peer) Copy() *Peer { SSHEnabled: p.SSHEnabled, LoginExpirationEnabled: p.LoginExpirationEnabled, LastLogin: p.LastLogin, + Ephemeral: p.Ephemeral, } } @@ -514,6 +517,7 @@ func (am *DefaultAccountManager) AddPeer(setupKey, userID string, peer *Peer) (* AccountID: account.Id, } + var ephemeral bool if !addedByUser { // validate the setup key if adding with a key sk, err := account.FindSetupKey(upperKey) @@ -528,6 +532,7 @@ func (am *DefaultAccountManager) AddPeer(setupKey, userID string, peer *Peer) (* account.SetupKeys[sk.Key] = sk.IncrementUsage() opEvent.InitiatorID = sk.Id opEvent.Activity = activity.PeerAddedWithSetupKey + ephemeral = sk.Ephemeral } else { opEvent.InitiatorID = userID opEvent.Activity = activity.PeerAddedByUser @@ -562,6 +567,7 @@ func (am *DefaultAccountManager) AddPeer(setupKey, userID string, peer *Peer) (* SSHKey: peer.SSHKey, LastLogin: time.Now().UTC(), LoginExpirationEnabled: addedByUser, + Ephemeral: ephemeral, } // add peer to 'All' group diff --git a/management/server/peer_test.go b/management/server/peer_test.go index 875aaeaba..822856e6a 100644 --- a/management/server/peer_test.go +++ b/management/server/peer_test.go @@ -78,7 +78,7 @@ func TestAccountManager_GetNetworkMap(t *testing.T) { t.Fatal(err) } - setupKey, err := manager.CreateSetupKey(account.Id, "test-key", SetupKeyReusable, time.Hour, nil, 999, userId) + setupKey, err := manager.CreateSetupKey(account.Id, "test-key", SetupKeyReusable, time.Hour, nil, 999, userId, false) if err != nil { return } @@ -331,7 +331,7 @@ func TestAccountManager_GetPeerNetwork(t *testing.T) { t.Fatal(err) } - setupKey, err := manager.CreateSetupKey(account.Id, "test-key", SetupKeyReusable, time.Hour, nil, 999, userId) + setupKey, err := manager.CreateSetupKey(account.Id, "test-key", SetupKeyReusable, time.Hour, nil, 999, userId, false) if err != nil { return } @@ -405,7 +405,7 @@ func TestDefaultAccountManager_GetPeer(t *testing.T) { } // two peers one added by a regular user and one with a setup key - setupKey, err := manager.CreateSetupKey(account.Id, "test-key", SetupKeyReusable, time.Hour, nil, 999, adminUser) + setupKey, err := manager.CreateSetupKey(account.Id, "test-key", SetupKeyReusable, time.Hour, nil, 999, adminUser, false) if err != nil { return } diff --git a/management/server/setupkey.go b/management/server/setupkey.go index ffdd822e3..e857230a5 100644 --- a/management/server/setupkey.go +++ b/management/server/setupkey.go @@ -86,6 +86,8 @@ type SetupKey struct { // UsageLimit indicates the number of times this key can be used to enroll a machine. // The value of 0 indicates the unlimited usage. UsageLimit int + // Ephemeral indicate if the peers will be ephemeral or not + Ephemeral bool } // Copy copies SetupKey to a new object @@ -108,6 +110,7 @@ func (key *SetupKey) Copy() *SetupKey { LastUsed: key.LastUsed, AutoGroups: autoGroups, UsageLimit: key.UsageLimit, + Ephemeral: key.Ephemeral, } } @@ -162,7 +165,7 @@ func (key *SetupKey) IsOverUsed() bool { // GenerateSetupKey generates a new setup key func GenerateSetupKey(name string, t SetupKeyType, validFor time.Duration, autoGroups []string, - usageLimit int) *SetupKey { + usageLimit int, ephemeral bool) *SetupKey { key := strings.ToUpper(uuid.New().String()) limit := usageLimit if t == SetupKeyOneOff { @@ -180,13 +183,14 @@ func GenerateSetupKey(name string, t SetupKeyType, validFor time.Duration, autoG UsedTimes: 0, AutoGroups: autoGroups, UsageLimit: limit, + Ephemeral: ephemeral, } } // GenerateDefaultSetupKey generates a default reusable setup key with an unlimited usage and 30 days expiration func GenerateDefaultSetupKey() *SetupKey { return GenerateSetupKey(DefaultSetupKeyName, SetupKeyReusable, DefaultSetupKeyDuration, []string{}, - SetupKeyUnlimitedUsage) + SetupKeyUnlimitedUsage, false) } func Hash(s string) uint32 { @@ -201,7 +205,7 @@ func Hash(s string) uint32 { // CreateSetupKey generates a new setup key with a given name, type, list of groups IDs to auto-assign to peers registered with this key, // and adds it to the specified account. A list of autoGroups IDs can be empty. func (am *DefaultAccountManager) CreateSetupKey(accountID string, keyName string, keyType SetupKeyType, - expiresIn time.Duration, autoGroups []string, usageLimit int, userID string) (*SetupKey, error) { + expiresIn time.Duration, autoGroups []string, usageLimit int, userID string, ephemeral bool) (*SetupKey, error) { unlock := am.Store.AcquireAccountLock(accountID) defer unlock() @@ -221,7 +225,7 @@ func (am *DefaultAccountManager) CreateSetupKey(accountID string, keyName string } } - setupKey := GenerateSetupKey(keyName, keyType, keyDuration, autoGroups, usageLimit) + setupKey := GenerateSetupKey(keyName, keyType, keyDuration, autoGroups, usageLimit, ephemeral) account.SetupKeys[setupKey.Key] = setupKey err = am.Store.SaveAccount(account) if err != nil { diff --git a/management/server/setupkey_test.go b/management/server/setupkey_test.go index a6f318ab9..6da01bd82 100644 --- a/management/server/setupkey_test.go +++ b/management/server/setupkey_test.go @@ -37,7 +37,7 @@ func TestDefaultAccountManager_SaveSetupKey(t *testing.T) { keyName := "my-test-key" key, err := manager.CreateSetupKey(account.Id, keyName, SetupKeyReusable, expiresIn, []string{}, - SetupKeyUnlimitedUsage, userID) + SetupKeyUnlimitedUsage, userID, false) if err != nil { t.Fatal(err) } @@ -136,7 +136,7 @@ func TestDefaultAccountManager_CreateSetupKey(t *testing.T) { for _, tCase := range []testCase{testCase1, testCase2} { t.Run(tCase.name, func(t *testing.T) { key, err := manager.CreateSetupKey(account.Id, tCase.expectedKeyName, SetupKeyReusable, expiresIn, - tCase.expectedGroups, SetupKeyUnlimitedUsage, userID) + tCase.expectedGroups, SetupKeyUnlimitedUsage, userID, false) if tCase.expectedFailure { if err == nil { @@ -193,7 +193,7 @@ func TestGenerateSetupKey(t *testing.T) { expectedUpdatedAt := time.Now().UTC() var expectedAutoGroups []string - key := GenerateSetupKey(expectedName, SetupKeyOneOff, time.Hour, []string{}, SetupKeyUnlimitedUsage) + key := GenerateSetupKey(expectedName, SetupKeyOneOff, time.Hour, []string{}, SetupKeyUnlimitedUsage, false) assertKey(t, key, expectedName, expectedRevoke, expectedType, expectedUsedTimes, expectedCreatedAt, expectedExpiresAt, strconv.Itoa(int(Hash(key.Key))), expectedUpdatedAt, expectedAutoGroups) @@ -201,33 +201,33 @@ func TestGenerateSetupKey(t *testing.T) { } func TestSetupKey_IsValid(t *testing.T) { - validKey := GenerateSetupKey("valid key", SetupKeyOneOff, time.Hour, []string{}, SetupKeyUnlimitedUsage) + validKey := GenerateSetupKey("valid key", SetupKeyOneOff, time.Hour, []string{}, SetupKeyUnlimitedUsage, false) if !validKey.IsValid() { t.Errorf("expected key to be valid, got invalid %v", validKey) } // expired - expiredKey := GenerateSetupKey("invalid key", SetupKeyOneOff, -time.Hour, []string{}, SetupKeyUnlimitedUsage) + expiredKey := GenerateSetupKey("invalid key", SetupKeyOneOff, -time.Hour, []string{}, SetupKeyUnlimitedUsage, false) if expiredKey.IsValid() { t.Errorf("expected key to be invalid due to expiration, got valid %v", expiredKey) } // revoked - revokedKey := GenerateSetupKey("invalid key", SetupKeyOneOff, time.Hour, []string{}, SetupKeyUnlimitedUsage) + revokedKey := GenerateSetupKey("invalid key", SetupKeyOneOff, time.Hour, []string{}, SetupKeyUnlimitedUsage, false) revokedKey.Revoked = true if revokedKey.IsValid() { t.Errorf("expected revoked key to be invalid, got valid %v", revokedKey) } // overused - overUsedKey := GenerateSetupKey("invalid key", SetupKeyOneOff, time.Hour, []string{}, SetupKeyUnlimitedUsage) + overUsedKey := GenerateSetupKey("invalid key", SetupKeyOneOff, time.Hour, []string{}, SetupKeyUnlimitedUsage, false) overUsedKey.UsedTimes = 1 if overUsedKey.IsValid() { t.Errorf("expected overused key to be invalid, got valid %v", overUsedKey) } // overused - reusableKey := GenerateSetupKey("valid key", SetupKeyReusable, time.Hour, []string{}, SetupKeyUnlimitedUsage) + reusableKey := GenerateSetupKey("valid key", SetupKeyReusable, time.Hour, []string{}, SetupKeyUnlimitedUsage, false) reusableKey.UsedTimes = 99 if !reusableKey.IsValid() { t.Errorf("expected reusable key to be valid when used many times, got valid %v", reusableKey) @@ -282,7 +282,7 @@ func assertKey(t *testing.T, key *SetupKey, expectedName string, expectedRevoke func TestSetupKey_Copy(t *testing.T) { - key := GenerateSetupKey("key name", SetupKeyOneOff, time.Hour, []string{}, SetupKeyUnlimitedUsage) + key := GenerateSetupKey("key name", SetupKeyOneOff, time.Hour, []string{}, SetupKeyUnlimitedUsage, false) keyCopy := key.Copy() assertKey(t, keyCopy, key.Name, key.Revoked, string(key.Type), key.UsedTimes, key.CreatedAt, key.ExpiresAt, key.Id,