Add login expiration fields to peer HTTP API (#687)

Return login expiration related fields in the Peer HTTP GET endpoint.
Support enable/disable peer's login expiration via HTTP PUT.
This commit is contained in:
Misha Bragin 2023-02-14 10:14:00 +01:00 committed by GitHub
parent b64f5ffcb4
commit 756ce96da9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 157 additions and 58 deletions

View File

@ -61,6 +61,10 @@ const (
PeerSSHDisabled
// PeerRenamed indicates that a user renamed a peer
PeerRenamed
// PeerLoginExpirationEnabled indicates that a user enabled login expiration of a peer
PeerLoginExpirationEnabled
// PeerLoginExpirationDisabled indicates that a user disabled login expiration of a peer
PeerLoginExpirationDisabled
// NameserverGroupCreated indicates that a user created a nameservers group
NameserverGroupCreated
// NameserverGroupDeleted indicates that a user deleted a nameservers group
@ -130,6 +134,10 @@ const (
PeerSSHDisabledMessage string = "Peer SSH server disabled"
// PeerRenamedMessage is a human-readable text message of the PeerRenamed activity
PeerRenamedMessage string = "Peer renamed"
// PeerLoginExpirationDisabledMessage is a human-readable text message of the PeerLoginExpirationDisabled activity
PeerLoginExpirationDisabledMessage string = "Peer login expiration disabled"
// PeerLoginExpirationEnabledMessage is a human-readable text message of the PeerLoginExpirationEnabled activity
PeerLoginExpirationEnabledMessage string = "Peer login expiration enabled"
// NameserverGroupCreatedMessage is a human-readable text message of the NameserverGroupCreated activity
NameserverGroupCreatedMessage string = "Nameserver group created"
// NameserverGroupDeletedMessage is a human-readable text message of the NameserverGroupDeleted activity
@ -202,6 +210,10 @@ func (a Activity) Message() string {
return PeerSSHEnabledMessage
case PeerSSHDisabled:
return PeerSSHDisabledMessage
case PeerLoginExpirationEnabled:
return PeerLoginExpirationEnabledMessage
case PeerLoginExpirationDisabled:
return PeerLoginExpirationDisabledMessage
case PeerRenamed:
return PeerRenamedMessage
case NameserverGroupCreated:
@ -278,6 +290,10 @@ func (a Activity) StringCode() string {
return "peer.ssh.enable"
case PeerSSHDisabled:
return "peer.ssh.disable"
case PeerLoginExpirationDisabled:
return "peer.login.expiration.disable"
case PeerLoginExpirationEnabled:
return "peer.login.expiration.enable"
case NameserverGroupCreated:
return "nameserver.group.add"
case NameserverGroupDeleted:

View File

@ -145,6 +145,16 @@ components:
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
login_expiration_enabled:
description: Indicates whether peer login expiration has been enabled or not
type: boolean
login_expired:
description: Indicates whether peer's login expired or not
type: boolean
last_login:
description: Last time this peer performed log in (authentication). E.g., user authenticated.
type: string
format: date-time
required:
- ip
- connected
@ -155,6 +165,9 @@ components:
- ssh_enabled
- hostname
- dns_label
- login_expiration_enabled
- login_expired
- last_login
SetupKey:
type: object
properties:
@ -542,7 +555,7 @@ components:
"account.create", "dns.setting.disabled.management.group.delete",
"route.add", "route.delete", "route.update",
"nameserver.group.add", "nameserver.group.delete", "nameserver.group.update",
"peer.ssh.disable", "peer.ssh.enable", "peer.rename"
"peer.ssh.disable", "peer.ssh.enable", "peer.rename", "peer.login.expiration.disable", "peer.login.expiration.enable"
]
initiator_id:
description: The ID of the initiator of the event. E.g., an ID of a user that triggered the event.
@ -741,7 +754,7 @@ paths:
type: string
description: The Peer ID
requestBody:
description: update to peers
description: update a peer
content:
'application/json':
schema:
@ -751,9 +764,12 @@ paths:
type: string
ssh_enabled:
type: boolean
login_expiration_enabled:
type: boolean
required:
- name
- ssh_enabled
- login_expiration_enabled
responses:
'200':
description: A Peer object

View File

@ -21,6 +21,8 @@ const (
EventActivityCodeNameserverGroupAdd EventActivityCode = "nameserver.group.add"
EventActivityCodeNameserverGroupDelete EventActivityCode = "nameserver.group.delete"
EventActivityCodeNameserverGroupUpdate EventActivityCode = "nameserver.group.update"
EventActivityCodePeerLoginExpirationDisable EventActivityCode = "peer.login.expiration.disable"
EventActivityCodePeerLoginExpirationEnable EventActivityCode = "peer.login.expiration.enable"
EventActivityCodePeerRename EventActivityCode = "peer.rename"
EventActivityCodePeerSshDisable EventActivityCode = "peer.ssh.disable"
EventActivityCodePeerSshEnable EventActivityCode = "peer.ssh.enable"
@ -326,9 +328,18 @@ type Peer struct {
// 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"`
@ -626,6 +637,7 @@ type PutApiGroupsIdJSONBody struct {
// PutApiPeersIdJSONBody defines parameters for PutApiPeersId.
type PutApiPeersIdJSONBody struct {
LoginExpirationEnabled bool `json:"login_expiration_enabled"`
Name string `json:"name"`
SshEnabled bool `json:"ssh_enabled"`
}

View File

@ -47,7 +47,8 @@ func (h *Peers) updatePeer(account *server.Account, user *server.User, peerID st
return
}
update := &server.Peer{ID: peerID, SSHEnabled: req.SshEnabled, Name: req.Name}
update := &server.Peer{ID: peerID, SSHEnabled: req.SshEnabled, Name: req.Name,
LoginExpirationEnabled: req.LoginExpirationEnabled}
peer, err := h.accountManager.UpdatePeer(account.Id, user.Id, update)
if err != nil {
util.WriteError(err, w)
@ -150,6 +151,9 @@ func toPeerResponse(peer *server.Peer, account *server.Account, dnsDomain string
if fqdn == "" {
fqdn = peer.DNSLabel
}
expired, _ := peer.LoginExpired(account.Settings)
return &api.Peer{
Id: peer.ID,
Name: peer.Name,
@ -164,5 +168,8 @@ func toPeerResponse(peer *server.Peer, account *server.Account, dnsDomain string
UserId: &peer.UserID,
UiVersion: &peer.Meta.UIVersion,
DnsLabel: fqdn,
LoginExpirationEnabled: peer.LoginExpirationEnabled,
LastLogin: peer.LastLogin,
LoginExpired: expired,
}
}

View File

@ -1,6 +1,7 @@
package http
import (
"bytes"
"encoding/json"
"github.com/gorilla/mux"
"io"
@ -8,6 +9,7 @@ import (
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/netbirdio/netbird/management/server/http/api"
@ -23,6 +25,13 @@ const testPeerID = "test_peer"
func initTestMetaData(peers ...*server.Peer) *Peers {
return &Peers{
accountManager: &mock_server.MockAccountManager{
UpdatePeerFunc: func(accountID, userID string, update *server.Peer) (*server.Peer, error) {
p := peers[0].Copy()
p.SSHEnabled = update.SSHEnabled
p.LoginExpirationEnabled = update.LoginExpirationEnabled
p.Name = update.Name
return p, nil
},
GetPeerFunc: func(accountID, peerID, userID string) (*server.Peer, error) {
return peers[0], nil
},
@ -40,6 +49,10 @@ func initTestMetaData(peers ...*server.Peer) *Peers {
Users: map[string]*server.User{
"test_user": user,
},
Settings: &server.Settings{
PeerLoginExpirationEnabled: true,
PeerLoginExpiration: time.Hour,
},
}, user, nil
},
},
@ -58,31 +71,7 @@ func initTestMetaData(peers ...*server.Peer) *Peers {
// Tests the GetPeers endpoint reachable in the route /api/peers
// Use the metadata generated by initTestMetaData() to check for values
func TestGetPeers(t *testing.T) {
tt := []struct {
name string
expectedStatus int
requestType string
requestPath string
requestBody io.Reader
expectedArray bool
}{
{
name: "GetPeersMetaData",
requestType: http.MethodGet,
requestPath: "/api/peers/",
expectedStatus: http.StatusOK,
expectedArray: true,
},
{
name: "GetPeer",
requestType: http.MethodGet,
requestPath: "/api/peers/" + testPeerID,
expectedStatus: http.StatusOK,
expectedArray: false,
},
}
rr := httptest.NewRecorder()
peer := &server.Peer{
ID: testPeerID,
Key: "key",
@ -90,6 +79,7 @@ func TestGetPeers(t *testing.T) {
IP: net.ParseIP("100.64.0.1"),
Status: &server.PeerStatus{},
Name: "PeerName",
LoginExpirationEnabled: false,
Meta: server.PeerSystemMeta{
Hostname: "hostname",
GoOS: "GoOS",
@ -101,6 +91,49 @@ func TestGetPeers(t *testing.T) {
},
}
expectedUpdatedPeer := peer.Copy()
expectedUpdatedPeer.LoginExpirationEnabled = true
expectedUpdatedPeer.SSHEnabled = true
expectedUpdatedPeer.Name = "New Name"
tt := []struct {
name string
expectedStatus int
requestType string
requestPath string
requestBody io.Reader
expectedArray bool
expectedPeer *server.Peer
}{
{
name: "GetPeersMetaData",
requestType: http.MethodGet,
requestPath: "/api/peers/",
expectedStatus: http.StatusOK,
expectedArray: true,
expectedPeer: peer,
},
{
name: "GetPeer",
requestType: http.MethodGet,
requestPath: "/api/peers/" + testPeerID,
expectedStatus: http.StatusOK,
expectedArray: false,
expectedPeer: peer,
},
{
name: "PutPeer",
requestType: http.MethodPut,
requestPath: "/api/peers/" + testPeerID,
expectedStatus: http.StatusOK,
expectedArray: false,
requestBody: bytes.NewBufferString("{\"login_expiration_enabled\":true,\"name\":\"New Name\",\"ssh_enabled\":true}"),
expectedPeer: expectedUpdatedPeer,
},
}
rr := httptest.NewRecorder()
p := initTestMetaData(peer)
for _, tc := range tt {
@ -112,6 +145,7 @@ func TestGetPeers(t *testing.T) {
router := mux.NewRouter()
router.HandleFunc("/api/peers/", p.GetPeers).Methods("GET")
router.HandleFunc("/api/peers/{id}", p.HandlePeer).Methods("GET")
router.HandleFunc("/api/peers/{id}", p.HandlePeer).Methods("PUT")
router.ServeHTTP(recorder, req)
res := recorder.Result()
@ -144,10 +178,12 @@ func TestGetPeers(t *testing.T) {
}
}
assert.Equal(t, got.Name, peer.Name)
assert.Equal(t, got.Version, peer.Meta.WtVersion)
assert.Equal(t, got.Ip, peer.IP.String())
assert.Equal(t, got.Name, tc.expectedPeer.Name)
assert.Equal(t, got.Version, tc.expectedPeer.Meta.WtVersion)
assert.Equal(t, got.Ip, tc.expectedPeer.IP.String())
assert.Equal(t, got.Os, "OS core")
assert.Equal(t, got.LoginExpirationEnabled, tc.expectedPeer.LoginExpirationEnabled)
assert.Equal(t, got.SshEnabled, tc.expectedPeer.SSHEnabled)
})
}
}

View File

@ -86,15 +86,17 @@ func (p *Peer) Copy() *Peer {
}
}
// LoginExpired indicates whether peer's login has expired or not.
// If Peer.LastLogin plus the expiresIn duration has happened already then login has expired.
// Return true if login has expired, false otherwise and time left to expiration (negative when expired).
// Expiration can be disabled/enabled on the Account or Peer level.
// LoginExpired indicates whether the peer's login has expired or not.
// If Peer.LastLogin plus the expiresIn duration has happened already; then login has expired.
// Return true if a login has expired, false otherwise, and time left to expiration (negative when expired).
// Login expiration can be disabled/enabled on a Peer level via Peer.LoginExpirationEnabled property.
// Login expiration can also be disabled/enabled globally on the Account level via Settings.PeerLoginExpirationEnabled
// and if disabled on the Account level, then Peer.LoginExpirationEnabled is ineffective.
func (p *Peer) LoginExpired(accountSettings *Settings) (bool, time.Duration) {
expiresAt := p.LastLogin.Add(accountSettings.PeerLoginExpiration)
now := time.Now()
left := expiresAt.Sub(now)
return accountSettings.PeerLoginExpirationEnabled && p.LoginExpirationEnabled && (left <= 0), left
timeLeft := expiresAt.Sub(now)
return accountSettings.PeerLoginExpirationEnabled && p.LoginExpirationEnabled && (timeLeft <= 0), timeLeft
}
// FQDN returns peers FQDN combined of the peer's DNS label and the system's DNS domain
@ -206,7 +208,7 @@ func (am *DefaultAccountManager) MarkPeerConnected(peerPubKey string, connected
return nil
}
// UpdatePeer updates peer. Only Peer.Name and Peer.SSHEnabled can be updated.
// UpdatePeer updates peer. Only Peer.Name, Peer.SSHEnabled, and Peer.LoginExpirationEnabled can be updated.
func (am *DefaultAccountManager) UpdatePeer(accountID, userID string, update *Peer) (*Peer, error) {
unlock := am.Store.AcquireAccountLock(accountID)
@ -246,6 +248,16 @@ func (am *DefaultAccountManager) UpdatePeer(accountID, userID string, update *Pe
am.storeEvent(userID, peer.ID, accountID, activity.PeerRenamed, peer.EventMeta(am.GetDNSDomain()))
}
if peer.LoginExpirationEnabled != update.LoginExpirationEnabled {
peer.LoginExpirationEnabled = update.LoginExpirationEnabled
event := activity.PeerLoginExpirationEnabled
if !update.LoginExpirationEnabled {
event = activity.PeerLoginExpirationDisabled
}
am.storeEvent(userID, peer.IP.String(), accountID, event, peer.EventMeta(am.GetDNSDomain()))
}
account.UpdatePeer(peer)
err = am.Store.SaveAccount(account)