mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-02 19:49:07 +02:00
Merge remote-tracking branch 'origin/main' into fix_debug_upload_url_from_mgmt
# Conflicts: # management/server/activity/codes.go # management/server/store/sql_store.go # management/server/store/sql_store_test.go # upload-server/server/server.go
This commit is contained in:
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,6 +175,63 @@ func TestNetworkMapComponents_NetworkResourceRoutes_RouterPeer(t *testing.T) {
|
||||
assert.NotEmpty(t, nm.RoutesFirewallRules, "router peer should have route firewall rules for the resource")
|
||||
}
|
||||
|
||||
// A receiver without a firewall asks Calculate to skip the route firewall
|
||||
// rules. Everything the rest of the sync consumes — routes, peers, peer
|
||||
// firewall rules — must come out unchanged.
|
||||
func TestNetworkMapComponents_SkipRouteFirewallRules(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
account := createComponentTestAccount()
|
||||
|
||||
// The shared fixture leaves peer-router-1 out of every peer ACL, so its
|
||||
// FirewallRules would be empty and the comparison below vacuous. Give the
|
||||
// router a policy of its own.
|
||||
account.Policies = append(account.Policies, &types.Policy{
|
||||
ID: "policy-router", Name: "Router connectivity", Enabled: true,
|
||||
Rules: []*types.PolicyRule{{
|
||||
ID: "rule-router", Name: "Allow all <-> router", Enabled: true,
|
||||
Action: types.PolicyTrafficActionAccept, Protocol: types.PolicyRuleProtocolALL,
|
||||
Bidirectional: true,
|
||||
Sources: []string{"group-all"}, Destinations: []string{"group-all"},
|
||||
}},
|
||||
})
|
||||
|
||||
validated := allPeersValidated(account)
|
||||
|
||||
components := account.GetPeerNetworkMapComponents(
|
||||
ctx,
|
||||
"peer-router-1",
|
||||
account.GetPeersCustomZone(ctx, "netbird.io"),
|
||||
nil,
|
||||
validated,
|
||||
account.GetResourcePoliciesMap(),
|
||||
account.GetResourceRoutersMap(),
|
||||
account.GetActiveGroupUsers(),
|
||||
)
|
||||
|
||||
full := components.Calculate(ctx)
|
||||
require.NotEmpty(t, full.RoutesFirewallRules, "baseline: router peer must get route firewall rules")
|
||||
require.NotEmpty(t, full.FirewallRules, "baseline: router peer must get peer firewall rules")
|
||||
|
||||
components.SkipRouteFirewallRules = true
|
||||
skipped := components.Calculate(ctx)
|
||||
|
||||
assert.Empty(t, skipped.RoutesFirewallRules, "route firewall rules must not be computed when skipped")
|
||||
assert.ElementsMatch(t, routeNetworks(full.Routes), routeNetworks(skipped.Routes),
|
||||
"skipping route firewall rules must not change the routes")
|
||||
assert.ElementsMatch(t, peerIDs(full.Peers), peerIDs(skipped.Peers),
|
||||
"skipping route firewall rules must not change the peers to connect")
|
||||
assert.Equal(t, full.FirewallRules, skipped.FirewallRules,
|
||||
"peer firewall rules are unrelated and must come out unchanged")
|
||||
}
|
||||
|
||||
func routeNetworks(routes []*nmdata.Route) []string {
|
||||
networks := make([]string, 0, len(routes))
|
||||
for _, r := range routes {
|
||||
networks = append(networks, r.Network.String())
|
||||
}
|
||||
return networks
|
||||
}
|
||||
|
||||
func TestNetworkMapComponents_NetworkResourceRoutes_UnrelatedPeer(t *testing.T) {
|
||||
account := createComponentTestAccount()
|
||||
validated := allPeersValidated(account)
|
||||
|
||||
@@ -29,7 +29,7 @@ type Policy struct {
|
||||
// ID of the policy'
|
||||
ID string `gorm:"primaryKey"`
|
||||
|
||||
PublicID string `json:"-"`
|
||||
PublicID string `json:"-" gorm:"index"`
|
||||
|
||||
// AccountID is a reference to Account that this object belongs
|
||||
AccountID string `json:"-" gorm:"index"`
|
||||
|
||||
@@ -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