mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-03 03:59:07 +02:00
Merge branch 'main' into embedded-vnc
# Conflicts: # client/ui/frontend/src/app.tsx # client/ui/frontend/src/modules/main/MainConnectionStatusSwitch.tsx # client/ui/i18n/locales/uk/common.json # go.sum
This commit is contained in:
@@ -404,7 +404,7 @@ func (a *Account) GetExpiredPeers() []*nbpeer.Peer {
|
||||
|
||||
// GetNextPeerExpiration returns the minimum duration in which the next peer of the account will expire if it was found.
|
||||
// If there is no peer that expires this function returns false and a duration of 0.
|
||||
// This function only considers peers that haven't been expired yet and that are connected.
|
||||
// This function only considers peers that haven't been expired yet, whether connected or not.
|
||||
func (a *Account) GetNextPeerExpiration() (time.Duration, bool) {
|
||||
peersWithExpiry := a.GetPeersWithExpiration()
|
||||
if len(peersWithExpiry) == 0 {
|
||||
@@ -412,8 +412,7 @@ func (a *Account) GetNextPeerExpiration() (time.Duration, bool) {
|
||||
}
|
||||
var nextExpiry *time.Duration
|
||||
for _, peer := range peersWithExpiry {
|
||||
// consider only connected peers because others will require login on connecting to the management server
|
||||
if peer.Status.LoginExpired || !peer.Status.Connected {
|
||||
if peer.Status.LoginExpired {
|
||||
continue
|
||||
}
|
||||
_, duration := peer.LoginExpired(a.Settings.PeerLoginExpiration)
|
||||
|
||||
@@ -3,6 +3,7 @@ package types
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Identity provider validation errors
|
||||
@@ -99,7 +100,16 @@ func (idp *IdentityProvider) Validate() error {
|
||||
}
|
||||
if idp.Issuer != "" {
|
||||
parsedURL, err := url.Parse(idp.Issuer)
|
||||
if err != nil || parsedURL.Scheme == "" || parsedURL.Host == "" {
|
||||
if err != nil || parsedURL.Host == "" {
|
||||
return ErrIdentityProviderIssuerInvalid
|
||||
}
|
||||
if parsedURL.Scheme != "https" {
|
||||
return ErrIdentityProviderIssuerInvalid
|
||||
}
|
||||
if parsedURL.User != nil {
|
||||
return ErrIdentityProviderIssuerInvalid
|
||||
}
|
||||
if strings.ContainsAny(idp.Issuer, "?#") {
|
||||
return ErrIdentityProviderIssuerInvalid
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,3 +135,54 @@ func TestIdentityProvider_Validate(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentityProvider_ValidateRejectsNonOriginIssuers(t *testing.T) {
|
||||
issuers := []string{
|
||||
"https://idp.example.com/realms/nb?foo=bar",
|
||||
"https://idp.example.com/realms/nb#section",
|
||||
"https://user:pass@idp.example.com",
|
||||
"ftp://idp.example.com",
|
||||
"ldap://idp.example.com",
|
||||
"http://idp.example.com",
|
||||
}
|
||||
|
||||
for _, issuer := range issuers {
|
||||
t.Run(issuer, func(t *testing.T) {
|
||||
idp := &IdentityProvider{
|
||||
Name: "test",
|
||||
Type: IdentityProviderTypeOIDC,
|
||||
Issuer: issuer,
|
||||
ClientID: "client-id",
|
||||
}
|
||||
assert.ErrorIs(t, idp.Validate(), ErrIdentityProviderIssuerInvalid)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentityProvider_ValidateAcceptsOriginAndPath(t *testing.T) {
|
||||
for _, issuer := range []string{"https://idp.example.com", "https://idp.example.com/realms/nb", "https://127.0.0.1:5556/dex"} {
|
||||
t.Run(issuer, func(t *testing.T) {
|
||||
idp := &IdentityProvider{
|
||||
Name: "test",
|
||||
Type: IdentityProviderTypeOIDC,
|
||||
Issuer: issuer,
|
||||
ClientID: "client-id",
|
||||
}
|
||||
assert.NoError(t, idp.Validate())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentityProviderValidateRejectsBareDelimiters(t *testing.T) {
|
||||
for _, issuer := range []string{"https://idp.example.com/realms/nb?", "https://idp.example.com/realms/nb#"} {
|
||||
t.Run(issuer, func(t *testing.T) {
|
||||
idp := &IdentityProvider{
|
||||
Name: "test",
|
||||
Type: IdentityProviderTypeOIDC,
|
||||
Issuer: issuer,
|
||||
ClientID: "client-id",
|
||||
}
|
||||
assert.ErrorIs(t, idp.Validate(), ErrIdentityProviderIssuerInvalid)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,6 +285,25 @@ func (u *User) EncryptSensitiveData(enc *crypt.FieldEncrypt) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func MaskEmail(email string) string {
|
||||
local, domain, found := strings.Cut(email, "@")
|
||||
if !found || local == "" || domain == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Runes, not bytes, so a non-ASCII local part is not cut mid-character.
|
||||
runes := []rune(local)
|
||||
|
||||
// Keeping the first two and the last needs a local part of at least four to
|
||||
// hide anything at all: at three or fewer those are the whole of it, and the
|
||||
// address would be recoverable in full from what is meant to conceal it.
|
||||
if len(runes) < 4 {
|
||||
return "****@" + domain
|
||||
}
|
||||
|
||||
return string(runes[:2]) + "****" + string(runes[len(runes)-1]) + "@" + domain
|
||||
}
|
||||
|
||||
// DecryptSensitiveData decrypts the user's sensitive fields (Email and Name) in place.
|
||||
func (u *User) DecryptSensitiveData(enc *crypt.FieldEncrypt) error {
|
||||
if enc == nil {
|
||||
|
||||
@@ -296,3 +296,144 @@ func TestUser_EncryptDecryptRoundTrip(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaskEmail(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
email string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "ordinary address keeps the first two, the last, and the domain",
|
||||
email: "admin@example.com",
|
||||
expected: "ad****n@example.com",
|
||||
},
|
||||
{
|
||||
name: "four characters is the shortest local part that reveals anything",
|
||||
email: "abcd@example.com",
|
||||
expected: "ab****d@example.com",
|
||||
},
|
||||
{
|
||||
name: "three character local part is masked whole, since a lead and tail would be all of it",
|
||||
email: "abc@example.com",
|
||||
expected: "****@example.com",
|
||||
},
|
||||
{
|
||||
name: "two character local part is masked whole",
|
||||
email: "ab@example.com",
|
||||
expected: "****@example.com",
|
||||
},
|
||||
{
|
||||
name: "single character local part is masked whole",
|
||||
email: "a@b.co",
|
||||
expected: "****@b.co",
|
||||
},
|
||||
{
|
||||
name: "mask width does not report the length it stands in for",
|
||||
email: "a.very.long.local.part@example.com",
|
||||
expected: "a.****t@example.com",
|
||||
},
|
||||
{
|
||||
name: "a local part far longer than the mask is still reduced to three characters",
|
||||
email: "finance.department.notifications.owner.account@example.com",
|
||||
expected: "fi****t@example.com",
|
||||
},
|
||||
{
|
||||
name: "plus addressing is masked along with the rest of the local part",
|
||||
email: "admin+netbird@example.com",
|
||||
expected: "ad****d@example.com",
|
||||
},
|
||||
{
|
||||
name: "separators inside the local part are not treated specially",
|
||||
email: "first.last-name_x@example.com",
|
||||
expected: "fi****x@example.com",
|
||||
},
|
||||
{
|
||||
name: "case is preserved rather than normalised",
|
||||
email: "Admin@Example.COM",
|
||||
expected: "Ad****n@Example.COM",
|
||||
},
|
||||
{
|
||||
name: "subdomains stay intact",
|
||||
email: "owner@mail.corp.example.com",
|
||||
expected: "ow****r@mail.corp.example.com",
|
||||
},
|
||||
{
|
||||
name: "german umlauts count as single characters",
|
||||
email: "müller@example.de",
|
||||
expected: "mü****r@example.de",
|
||||
},
|
||||
{
|
||||
name: "cyrillic local part is cut on runes",
|
||||
email: "иванов@example.ru",
|
||||
expected: "ив****в@example.ru",
|
||||
},
|
||||
{
|
||||
name: "cjk local part of three runes is masked whole, counted in runes not bytes",
|
||||
email: "用户名@example.cn",
|
||||
expected: "****@example.cn",
|
||||
},
|
||||
{
|
||||
name: "cjk local part of four runes reveals the first two and the last",
|
||||
email: "用户名字@example.cn",
|
||||
expected: "用户****字@example.cn",
|
||||
},
|
||||
{
|
||||
name: "arabic local part is cut on runes",
|
||||
email: "مستخدم@example.sa",
|
||||
expected: "مس****م@example.sa",
|
||||
},
|
||||
{
|
||||
name: "two rune non-ascii local part is masked whole",
|
||||
email: "ää@example.de",
|
||||
expected: "****@example.de",
|
||||
},
|
||||
{
|
||||
name: "astral plane runes are not split into surrogates",
|
||||
email: "a🎉bc@example.com",
|
||||
expected: "a🎉****c@example.com",
|
||||
},
|
||||
{
|
||||
name: "a non-ascii domain is left alone",
|
||||
email: "admin@münchen.example",
|
||||
expected: "ad****n@münchen.example",
|
||||
},
|
||||
{
|
||||
name: "only the first separator splits, so a second stays in the domain",
|
||||
email: "a@b@example.com",
|
||||
expected: "****@b@example.com",
|
||||
},
|
||||
{
|
||||
name: "empty email has nothing to mask",
|
||||
email: "",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "value without a separator is not an address",
|
||||
email: "not-an-email",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "missing local part is not an address",
|
||||
email: "@example.com",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "missing domain is not an address",
|
||||
email: "admin@",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "a bare separator is not an address",
|
||||
email: "@",
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert.Equal(t, tc.expected, MaskEmail(tc.email))
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user