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

View File

@ -145,6 +145,16 @@ components:
dns_label: 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 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 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: required:
- ip - ip
- connected - connected
@ -155,6 +165,9 @@ components:
- ssh_enabled - ssh_enabled
- hostname - hostname
- dns_label - dns_label
- login_expiration_enabled
- login_expired
- last_login
SetupKey: SetupKey:
type: object type: object
properties: properties:
@ -542,7 +555,7 @@ components:
"account.create", "dns.setting.disabled.management.group.delete", "account.create", "dns.setting.disabled.management.group.delete",
"route.add", "route.delete", "route.update", "route.add", "route.delete", "route.update",
"nameserver.group.add", "nameserver.group.delete", "nameserver.group.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: initiator_id:
description: The ID of the initiator of the event. E.g., an ID of a user that triggered the event. 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 type: string
description: The Peer ID description: The Peer ID
requestBody: requestBody:
description: update to peers description: update a peer
content: content:
'application/json': 'application/json':
schema: schema:
@ -751,9 +764,12 @@ paths:
type: string type: string
ssh_enabled: ssh_enabled:
type: boolean type: boolean
login_expiration_enabled:
type: boolean
required: required:
- name - name
- ssh_enabled - ssh_enabled
- login_expiration_enabled
responses: responses:
'200': '200':
description: A Peer object description: A Peer object

View File

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

View File

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

View File

@ -1,6 +1,7 @@
package http package http
import ( import (
"bytes"
"encoding/json" "encoding/json"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"io" "io"
@ -8,6 +9,7 @@ import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
"time"
"github.com/netbirdio/netbird/management/server/http/api" "github.com/netbirdio/netbird/management/server/http/api"
@ -23,6 +25,13 @@ const testPeerID = "test_peer"
func initTestMetaData(peers ...*server.Peer) *Peers { func initTestMetaData(peers ...*server.Peer) *Peers {
return &Peers{ return &Peers{
accountManager: &mock_server.MockAccountManager{ 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) { GetPeerFunc: func(accountID, peerID, userID string) (*server.Peer, error) {
return peers[0], nil return peers[0], nil
}, },
@ -40,6 +49,10 @@ func initTestMetaData(peers ...*server.Peer) *Peers {
Users: map[string]*server.User{ Users: map[string]*server.User{
"test_user": user, "test_user": user,
}, },
Settings: &server.Settings{
PeerLoginExpirationEnabled: true,
PeerLoginExpiration: time.Hour,
},
}, user, nil }, user, nil
}, },
}, },
@ -58,31 +71,7 @@ func initTestMetaData(peers ...*server.Peer) *Peers {
// Tests the GetPeers endpoint reachable in the route /api/peers // Tests the GetPeers endpoint reachable in the route /api/peers
// Use the metadata generated by initTestMetaData() to check for values // Use the metadata generated by initTestMetaData() to check for values
func TestGetPeers(t *testing.T) { 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{ peer := &server.Peer{
ID: testPeerID, ID: testPeerID,
Key: "key", Key: "key",
@ -90,6 +79,7 @@ func TestGetPeers(t *testing.T) {
IP: net.ParseIP("100.64.0.1"), IP: net.ParseIP("100.64.0.1"),
Status: &server.PeerStatus{}, Status: &server.PeerStatus{},
Name: "PeerName", Name: "PeerName",
LoginExpirationEnabled: false,
Meta: server.PeerSystemMeta{ Meta: server.PeerSystemMeta{
Hostname: "hostname", Hostname: "hostname",
GoOS: "GoOS", 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) p := initTestMetaData(peer)
for _, tc := range tt { for _, tc := range tt {
@ -112,6 +145,7 @@ func TestGetPeers(t *testing.T) {
router := mux.NewRouter() router := mux.NewRouter()
router.HandleFunc("/api/peers/", p.GetPeers).Methods("GET") 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("GET")
router.HandleFunc("/api/peers/{id}", p.HandlePeer).Methods("PUT")
router.ServeHTTP(recorder, req) router.ServeHTTP(recorder, req)
res := recorder.Result() res := recorder.Result()
@ -144,10 +178,12 @@ func TestGetPeers(t *testing.T) {
} }
} }
assert.Equal(t, got.Name, peer.Name) assert.Equal(t, got.Name, tc.expectedPeer.Name)
assert.Equal(t, got.Version, peer.Meta.WtVersion) assert.Equal(t, got.Version, tc.expectedPeer.Meta.WtVersion)
assert.Equal(t, got.Ip, peer.IP.String()) assert.Equal(t, got.Ip, tc.expectedPeer.IP.String())
assert.Equal(t, got.Os, "OS core") 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. // 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. // 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). // Return true if a login has expired, false otherwise, and time left to expiration (negative when expired).
// Expiration can be disabled/enabled on the Account or Peer level. // 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) { func (p *Peer) LoginExpired(accountSettings *Settings) (bool, time.Duration) {
expiresAt := p.LastLogin.Add(accountSettings.PeerLoginExpiration) expiresAt := p.LastLogin.Add(accountSettings.PeerLoginExpiration)
now := time.Now() now := time.Now()
left := expiresAt.Sub(now) timeLeft := expiresAt.Sub(now)
return accountSettings.PeerLoginExpirationEnabled && p.LoginExpirationEnabled && (left <= 0), left 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 // 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 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) { func (am *DefaultAccountManager) UpdatePeer(accountID, userID string, update *Peer) (*Peer, error) {
unlock := am.Store.AcquireAccountLock(accountID) 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())) 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) account.UpdatePeer(peer)
err = am.Store.SaveAccount(account) err = am.Store.SaveAccount(account)