From 8be6e92563334489c11afe48d3faa32fee18ebc6 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Tue, 7 Nov 2023 14:38:36 +0100 Subject: [PATCH] Extend API with accessible peers (#1284) Extend the peer and peers API endpoints with accessible peers. --- management/server/http/api/openapi.yml | 48 ++++++- management/server/http/api/types.gen.go | 126 +++++++++++++++++++ management/server/http/peers_handler.go | 103 +++++++++++++-- management/server/http/peers_handler_test.go | 9 ++ 4 files changed, 271 insertions(+), 15 deletions(-) diff --git a/management/server/http/api/openapi.yml b/management/server/http/api/openapi.yml index a0a64fd98..64e97426a 100644 --- a/management/server/http/api/openapi.yml +++ b/management/server/http/api/openapi.yml @@ -217,7 +217,7 @@ components: - name - ssh_enabled - login_expiration_enabled - Peer: + PeerBase: allOf: - $ref: '#/components/schemas/PeerMinimum' - type: object @@ -294,6 +294,50 @@ components: - login_expiration_enabled - login_expired - last_login + AccessiblePeer: + allOf: + - $ref: '#/components/schemas/PeerMinimum' + - type: object + properties: + ip: + description: Peer's IP address + type: string + example: 10.64.0.1 + dns_label: + description: Peer's DNS label is the parsed peer name for domain resolution. It is used to form an FQDN by appending the account's domain to the peer label. e.g. peer-dns-label.netbird.cloud + type: string + example: stage-host-1.netbird.cloud + user_id: + description: User ID of the user that enrolled this peer + type: string + example: google-oauth2|277474792786460067937 + required: + - ip + - dns_label + - user_id + Peer: + allOf: + - $ref: '#/components/schemas/PeerBase' + - type: object + properties: + accessible_peers: + description: List of accessible peers + type: array + items: + $ref: '#/components/schemas/AccessiblePeer' + required: + - accessible_peers + PeerBatch: + allOf: + - $ref: '#/components/schemas/PeerBase' + - type: object + properties: + accessible_peers_count: + description: Number of accessible peers + type: integer + example: 5 + required: + - accessible_peers_count SetupKey: type: object properties: @@ -1364,7 +1408,7 @@ paths: schema: type: array items: - $ref: '#/components/schemas/Peer' + $ref: '#/components/schemas/PeerBatch' '400': "$ref": "#/components/responses/bad_request" '401': diff --git a/management/server/http/api/types.gen.go b/management/server/http/api/types.gen.go index ddf8ce65f..ea70b7f3a 100644 --- a/management/server/http/api/types.gen.go +++ b/management/server/http/api/types.gen.go @@ -117,6 +117,24 @@ const ( UserStatusInvited UserStatus = "invited" ) +// AccessiblePeer defines model for AccessiblePeer. +type AccessiblePeer struct { + // DnsLabel Peer's DNS label is the parsed peer name for domain resolution. It is used to form an FQDN by appending the account's domain to the peer label. e.g. peer-dns-label.netbird.cloud + DnsLabel string `json:"dns_label"` + + // Id Peer ID + Id string `json:"id"` + + // Ip Peer's IP address + Ip string `json:"ip"` + + // Name Peer's hostname + Name string `json:"name"` + + // UserId User ID of the user that enrolled this peer + UserId string `json:"user_id"` +} + // Account defines model for Account. type Account struct { // Id Account ID @@ -302,6 +320,114 @@ type NameserverGroupRequest struct { // Peer defines model for Peer. type Peer struct { + // AccessiblePeers List of accessible peers + AccessiblePeers []AccessiblePeer `json:"accessible_peers"` + + // Connected Peer to Management connection status + Connected bool `json:"connected"` + + // DnsLabel Peer's DNS label is the parsed peer name for domain resolution. It is used to form an FQDN by appending the account's domain to the peer label. e.g. peer-dns-label.netbird.cloud + DnsLabel string `json:"dns_label"` + + // Groups Groups that the peer belongs to + Groups []GroupMinimum `json:"groups"` + + // Hostname Hostname of the machine + Hostname string `json:"hostname"` + + // Id Peer ID + Id string `json:"id"` + + // Ip Peer's IP address + Ip string `json:"ip"` + + // LastLogin Last time this peer performed log in (authentication). E.g., user authenticated. + LastLogin time.Time `json:"last_login"` + + // LastSeen Last time peer connected to Netbird's management service + LastSeen time.Time `json:"last_seen"` + + // LoginExpirationEnabled Indicates whether peer login expiration has been enabled or not + LoginExpirationEnabled bool `json:"login_expiration_enabled"` + + // LoginExpired Indicates whether peer's login expired or not + LoginExpired bool `json:"login_expired"` + + // Name Peer's hostname + Name string `json:"name"` + + // Os Peer's operating system and version + Os string `json:"os"` + + // SshEnabled Indicates whether SSH server is enabled on this peer + SshEnabled bool `json:"ssh_enabled"` + + // UiVersion Peer's desktop UI version + UiVersion *string `json:"ui_version,omitempty"` + + // UserId User ID of the user that enrolled this peer + UserId *string `json:"user_id,omitempty"` + + // Version Peer's daemon or cli version + Version string `json:"version"` +} + +// PeerBase defines model for PeerBase. +type PeerBase struct { + // Connected Peer to Management connection status + Connected bool `json:"connected"` + + // DnsLabel Peer's DNS label is the parsed peer name for domain resolution. It is used to form an FQDN by appending the account's domain to the peer label. e.g. peer-dns-label.netbird.cloud + DnsLabel string `json:"dns_label"` + + // Groups Groups that the peer belongs to + Groups []GroupMinimum `json:"groups"` + + // Hostname Hostname of the machine + Hostname string `json:"hostname"` + + // Id Peer ID + Id string `json:"id"` + + // Ip Peer's IP address + Ip string `json:"ip"` + + // LastLogin Last time this peer performed log in (authentication). E.g., user authenticated. + LastLogin time.Time `json:"last_login"` + + // LastSeen Last time peer connected to Netbird's management service + LastSeen time.Time `json:"last_seen"` + + // LoginExpirationEnabled Indicates whether peer login expiration has been enabled or not + LoginExpirationEnabled bool `json:"login_expiration_enabled"` + + // LoginExpired Indicates whether peer's login expired or not + LoginExpired bool `json:"login_expired"` + + // Name Peer's hostname + Name string `json:"name"` + + // Os Peer's operating system and version + Os string `json:"os"` + + // SshEnabled Indicates whether SSH server is enabled on this peer + SshEnabled bool `json:"ssh_enabled"` + + // UiVersion Peer's desktop UI version + UiVersion *string `json:"ui_version,omitempty"` + + // UserId User ID of the user that enrolled this peer + UserId *string `json:"user_id,omitempty"` + + // Version Peer's daemon or cli version + Version string `json:"version"` +} + +// PeerBatch defines model for PeerBatch. +type PeerBatch struct { + // AccessiblePeersCount Number of accessible peers + AccessiblePeersCount int `json:"accessible_peers_count"` + // Connected Peer to Management connection status Connected bool `json:"connected"` diff --git a/management/server/http/peers_handler.go b/management/server/http/peers_handler.go index a485d6ccf..3d0d735ec 100644 --- a/management/server/http/peers_handler.go +++ b/management/server/http/peers_handler.go @@ -61,8 +61,14 @@ func (h *PeersHandler) getPeer(account *server.Account, peerID, userID string, w util.WriteError(err, w) return } + dnsDomain := h.accountManager.GetDNSDomain() - util.WriteJSONObject(w, toPeerResponse(peerToReturn, account, h.accountManager.GetDNSDomain())) + groupsInfo := toGroupsInfo(account.Groups, peer.ID) + + netMap := account.GetPeerNetworkMap(peerID, h.accountManager.GetDNSDomain()) + accessiblePeers := toAccessiblePeers(netMap, dnsDomain) + + util.WriteJSONObject(w, toSinglePeerResponse(peerToReturn, groupsInfo, dnsDomain, accessiblePeers)) } func (h *PeersHandler) updatePeer(account *server.Account, user *server.User, peerID string, w http.ResponseWriter, r *http.Request) { @@ -81,7 +87,13 @@ func (h *PeersHandler) updatePeer(account *server.Account, user *server.User, pe return } dnsDomain := h.accountManager.GetDNSDomain() - util.WriteJSONObject(w, toPeerResponse(peer, account, dnsDomain)) + + groupMinimumInfo := toGroupsInfo(account.Groups, peer.ID) + + netMap := account.GetPeerNetworkMap(peerID, h.accountManager.GetDNSDomain()) + accessiblePeers := toAccessiblePeers(netMap, dnsDomain) + + util.WriteJSONObject(w, toSinglePeerResponse(peer, groupMinimumInfo, dnsDomain, accessiblePeers)) } func (h *PeersHandler) deletePeer(accountID, userID string, peerID string, w http.ResponseWriter) { @@ -142,14 +154,18 @@ func (h *PeersHandler) GetAllPeers(w http.ResponseWriter, r *http.Request) { dnsDomain := h.accountManager.GetDNSDomain() - respBody := []*api.Peer{} + respBody := make([]*api.PeerBatch, 0, len(peers)) for _, peer := range peers { peerToReturn, err := h.checkPeerStatus(peer) if err != nil { util.WriteError(err, w) return } - respBody = append(respBody, toPeerResponse(peerToReturn, account, dnsDomain)) + groupMinimumInfo := toGroupsInfo(account.Groups, peer.ID) + + accessiblePeerNumbers := h.accessiblePeersNumber(account, peer.ID) + + respBody = append(respBody, toPeerListItemResponse(peerToReturn, groupMinimumInfo, dnsDomain, accessiblePeerNumbers)) } util.WriteJSONObject(w, respBody) return @@ -158,17 +174,48 @@ func (h *PeersHandler) GetAllPeers(w http.ResponseWriter, r *http.Request) { } } -func toPeerResponse(peer *server.Peer, account *server.Account, dnsDomain string) *api.Peer { +func (h *PeersHandler) accessiblePeersNumber(account *server.Account, peerID string) int { + netMap := account.GetPeerNetworkMap(peerID, h.accountManager.GetDNSDomain()) + return len(netMap.Peers) + len(netMap.OfflinePeers) +} + +func toAccessiblePeers(netMap *server.NetworkMap, dnsDomain string) []api.AccessiblePeer { + accessiblePeers := make([]api.AccessiblePeer, 0, len(netMap.Peers)+len(netMap.OfflinePeers)) + for _, p := range netMap.Peers { + ap := api.AccessiblePeer{ + Id: p.ID, + Name: p.Name, + Ip: p.IP.String(), + DnsLabel: fqdn(p, dnsDomain), + UserId: p.UserID, + } + accessiblePeers = append(accessiblePeers, ap) + } + + for _, p := range netMap.OfflinePeers { + ap := api.AccessiblePeer{ + Id: p.ID, + Name: p.Name, + Ip: p.IP.String(), + DnsLabel: fqdn(p, dnsDomain), + UserId: p.UserID, + } + accessiblePeers = append(accessiblePeers, ap) + } + return accessiblePeers +} + +func toGroupsInfo(groups map[string]*server.Group, peerID string) []api.GroupMinimum { var groupsInfo []api.GroupMinimum groupsChecked := make(map[string]struct{}) - for _, group := range account.Groups { + for _, group := range groups { _, ok := groupsChecked[group.ID] if ok { continue } groupsChecked[group.ID] = struct{}{} for _, pk := range group.Peers { - if pk == peer.ID { + if pk != peerID { info := api.GroupMinimum{ Id: group.ID, Name: group.Name, @@ -179,12 +226,10 @@ func toPeerResponse(peer *server.Peer, account *server.Account, dnsDomain string } } } + return groupsInfo +} - fqdn := peer.FQDN(dnsDomain) - if fqdn == "" { - fqdn = peer.DNSLabel - } - +func toSinglePeerResponse(peer *server.Peer, groupsInfo []api.GroupMinimum, dnsDomain string, accessiblePeer []api.AccessiblePeer) *api.Peer { return &api.Peer{ Id: peer.ID, Name: peer.Name, @@ -198,9 +243,41 @@ func toPeerResponse(peer *server.Peer, account *server.Account, dnsDomain string Hostname: peer.Meta.Hostname, UserId: &peer.UserID, UiVersion: &peer.Meta.UIVersion, - DnsLabel: fqdn, + DnsLabel: fqdn(peer, dnsDomain), LoginExpirationEnabled: peer.LoginExpirationEnabled, LastLogin: peer.LastLogin, LoginExpired: peer.Status.LoginExpired, + AccessiblePeers: accessiblePeer, + } +} + +func toPeerListItemResponse(peer *server.Peer, groupsInfo []api.GroupMinimum, dnsDomain string, accessiblePeersCount int) *api.PeerBatch { + return &api.PeerBatch{ + Id: peer.ID, + Name: peer.Name, + Ip: peer.IP.String(), + Connected: peer.Status.Connected, + LastSeen: peer.Status.LastSeen, + Os: fmt.Sprintf("%s %s", peer.Meta.OS, peer.Meta.Core), + Version: peer.Meta.WtVersion, + Groups: groupsInfo, + SshEnabled: peer.SSHEnabled, + Hostname: peer.Meta.Hostname, + UserId: &peer.UserID, + UiVersion: &peer.Meta.UIVersion, + DnsLabel: fqdn(peer, dnsDomain), + LoginExpirationEnabled: peer.LoginExpirationEnabled, + LastLogin: peer.LastLogin, + LoginExpired: peer.Status.LoginExpired, + AccessiblePeersCount: accessiblePeersCount, + } +} + +func fqdn(peer *server.Peer, dnsDomain string) string { + fqdn := peer.FQDN(dnsDomain) + if fqdn == "" { + return peer.DNSLabel + } else { + return fqdn } } diff --git a/management/server/http/peers_handler_test.go b/management/server/http/peers_handler_test.go index 1856861d5..e61c6905c 100644 --- a/management/server/http/peers_handler_test.go +++ b/management/server/http/peers_handler_test.go @@ -70,8 +70,17 @@ func initTestMetaData(peers ...*server.Peer) *PeersHandler { PeerLoginExpirationEnabled: true, PeerLoginExpiration: time.Hour, }, + Network: &server.Network{ + Identifier: "ciclqisab2ss43jdn8q0", + Net: net.IPNet{ + IP: net.ParseIP("100.67.0.0"), + Mask: net.IPv4Mask(255, 255, 0, 0), + }, + Serial: 51, + }, }, user, nil }, + GetAllConnectedPeersFunc: func() (map[string]struct{}, error) { statuses := make(map[string]struct{}) for _, peer := range peers {