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:
riccardom
2026-09-28 10:56:27 +02:00
320 changed files with 25425 additions and 12010 deletions
+11 -1
View File
@@ -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)
+1 -1
View File
@@ -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"`
+19
View File
@@ -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 {
+141
View File
@@ -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))
})
}
}