Files
netbird/shared/management/networkmap/networkmapcompute.go
T

777 lines
23 KiB
Go

package networkmap
import (
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
"github.com/netbirdio/netbird/shared/management/types"
)
type sshRequirements struct {
neededGroupIDs map[string]struct{}
needAllowedUserIDs bool
}
// GetPeerNetworkMapComponents computes the peer's NetworkMapComponents from the
// slim twin store. It mirrors the former Account.GetPeerNetworkMapComponents
// exactly, operating on nmdata twins throughout — no Account reference and no
// twin↔real conversion, since the produced components hold twins.
func (nmd *NetworkMapData) GetPeerNetworkMapComponents(peerID string, peersCustomZone nmdata.CustomZone) *types.NetworkMapComponents {
nmd.InjectProxyPolicies()
forceRoutingPeerDNS := nmd.forcesRoutingPeerDNSResolution(peerID)
peer := nmd.Peers[peerID]
if peer == nil {
return types.EmptyNetworkMapComponents(&types.NetworkMapComponents{
PeerID: peerID,
Network: nmd.Network,
Peers: map[string]*nmdata.Peer{peerID: peer},
ForceRoutingPeerDNSResolution: forceRoutingPeerDNS,
})
}
if _, ok := nmd.ValidatedPeers[peerID]; !ok {
return types.EmptyNetworkMapComponents(&types.NetworkMapComponents{
PeerID: peerID,
Network: nmd.Network,
Peers: map[string]*nmdata.Peer{peerID: peer},
ForceRoutingPeerDNSResolution: forceRoutingPeerDNS,
})
}
components := &types.NetworkMapComponents{
PeerID: peerID,
Network: nmd.Network,
AccountSettings: nmd.AccountSettings,
DNSSettings: nmd.DNSSettings,
CustomZoneDomain: peersCustomZone.Domain,
NameServerGroups: make([]*nmdata.NameServerGroup, 0),
ResourcePoliciesMap: make(map[string][]*nmdata.Policy),
RoutersMap: make(map[string]map[string]*nmdata.NetworkRouter),
NetworkResources: make([]*nmdata.NetworkResource, 0),
PostureFailedPeers: make(map[string]map[string]struct{}, len(nmd.PostureChecks)),
RouterPeers: make(map[string]*nmdata.Peer),
NetworkXIDToPublicID: nmd.NetworkXIDToPublicID,
PostureCheckXIDToPublicID: nmd.PostureCheckXIDToPublicID,
ForceRoutingPeerDNSResolution: forceRoutingPeerDNS,
}
relevantPeers, relevantGroups, relevantPolicies, relevantRoutes, sshReqs := nmd.getPeersGroupsPoliciesRoutes(peerID, peer.SSHEnabled)
if len(sshReqs.neededGroupIDs) > 0 {
components.GroupIDToUserIDs = filterGroupIDToUserIDs(nmd.GroupIDToUserIDs, sshReqs.neededGroupIDs)
}
if sshReqs.needAllowedUserIDs {
components.AllowedUserIDs = nmd.getAllowedUserIDs()
}
components.Peers = relevantPeers
components.Groups = relevantGroups
components.Policies = relevantPolicies
components.Routes = relevantRoutes
components.AllDNSRecords = filterDNSRecordsByPeers(peersCustomZone.Records, relevantPeers, peer.SupportsIPv6() && peer.IPv6.IsValid())
peerGroups := nmd.GetPeerGroups(peerID)
components.AccountZones = nmd.appliedZones(peerGroups)
components.AccountZones = append(components.AccountZones, nmd.privateServiceZones(peerGroups)...)
for _, nsGroup := range nmd.NameServerGroups {
if nsGroup != nil && nsGroup.Enabled {
for _, gID := range nsGroup.Groups {
if _, found := relevantGroups[gID]; found {
components.NameServerGroups = append(components.NameServerGroups, nsGroup)
break
}
}
}
}
seenSourcePolicies := make(map[*nmdata.Policy]struct{})
var sourceFailures []map[string]map[string]struct{}
for _, resource := range nmd.NetworkResources {
if resource == nil || !resource.Enabled {
continue
}
policies, exists := nmd.ResourcePolicies[resource.ID]
if !exists {
continue
}
addSourcePeers := false
networkRoutingPeers, routerExists := nmd.Routers[resource.NetworkID]
if routerExists {
if _, ok := networkRoutingPeers[peerID]; ok {
addSourcePeers = true
}
}
for _, policy := range policies {
if policy == nil || !policy.Enabled || len(policy.Rules) == 0 || policy.Rules[0] == nil {
continue
}
if addSourcePeers {
if _, seen := seenSourcePolicies[policy]; !seen {
seenSourcePolicies[policy] = struct{}{}
sources := nmd.resourcePolicySources(policy)
nmd.mergeSourcePeers(components, sources)
sourceFailures = append(sourceFailures, sources.failed)
}
} else {
peerInSources := false
if policy.Rules[0].SourceResource.Type == string(types.ResourceTypePeer) && policy.Rules[0].SourceResource.ID != "" {
peerInSources = policy.Rules[0].SourceResource.ID == peerID
} else {
for _, groupID := range policy.SourceGroups() {
if _, ok := peerGroups[groupID]; ok {
peerInSources = true
break
}
}
}
if !peerInSources {
continue
}
isValid, pname := nmd.validatePostureChecksOnPeerGetFailed(policy.SourcePostureChecks, peerID)
if !isValid && len(pname) > 0 {
if _, ok := components.PostureFailedPeers[pname]; !ok {
components.PostureFailedPeers[pname] = make(map[string]struct{})
}
components.PostureFailedPeers[pname][peer.ID] = struct{}{}
continue
}
addSourcePeers = true
}
for _, rule := range policy.Rules {
if rule == nil || !rule.Enabled {
continue
}
for _, srcGroupID := range rule.Sources {
if g := nmd.Groups[srcGroupID]; g != nil {
if _, exists := components.Groups[srcGroupID]; !exists {
components.Groups[srcGroupID] = g
}
}
}
for _, dstGroupID := range rule.Destinations {
if g := nmd.Groups[dstGroupID]; g != nil {
if _, exists := components.Groups[dstGroupID]; !exists {
components.Groups[dstGroupID] = g
}
}
}
}
components.ResourcePoliciesMap[resource.ID] = policies
}
if addSourcePeers {
components.RoutersMap[resource.NetworkID] = networkRoutingPeers
for peerIDKey := range networkRoutingPeers {
p := nmd.Peers[peerIDKey]
if p == nil {
continue
}
// An unapproved peer must not carry traffic, so it is kept out of
// RouterPeers as well: the envelope encoder indexes that map into
// the wire peer table, from which the client restores every entry.
if _, validated := nmd.ValidatedPeers[peerIDKey]; !validated {
continue
}
if _, exists := components.RouterPeers[peerIDKey]; !exists {
components.RouterPeers[peerIDKey] = p
}
if _, exists := components.Peers[peerIDKey]; !exists {
components.Peers[peerIDKey] = p
}
}
components.NetworkResources = append(components.NetworkResources, resource)
}
}
filterGroupPeers(&components.Groups, components.Peers)
nmd.collectPostureFailedPeers(components, sourceFailures)
return components
}
func (nmd *NetworkMapData) getPeersGroupsPoliciesRoutes(
peerID string,
peerSSHEnabled bool,
) (map[string]*nmdata.Peer, map[string]*nmdata.Group, []*nmdata.Policy, []*nmdata.Route, sshRequirements) {
relevantPeerIDs := make(map[string]*nmdata.Peer, len(nmd.Peers)/4)
relevantGroupIDs := make(map[string]*nmdata.Group, len(nmd.Groups)/4)
relevantPolicies := make([]*nmdata.Policy, 0, len(nmd.Policies))
relevantRoutes := make([]*nmdata.Route, 0, len(nmd.Routes))
sshReqs := sshRequirements{neededGroupIDs: make(map[string]struct{})}
relevantPeerIDs[peerID] = nmd.Peers[peerID]
peerGroupSet := nmd.GetPeerGroups(peerID)
for groupID := range peerGroupSet {
relevantGroupIDs[groupID] = nmd.Groups[groupID]
}
routeAccessControlGroups := make(map[string]struct{})
for _, r := range nmd.Routes {
if r == nil {
continue
}
relevant := r.Peer == peerID
if !relevant {
for _, groupID := range r.PeerGroups {
if _, ok := peerGroupSet[groupID]; ok {
relevant = true
break
}
}
}
if !relevant && r.Enabled {
for _, groupID := range r.Groups {
if _, ok := peerGroupSet[groupID]; ok {
relevant = true
break
}
}
}
if !relevant {
continue
}
for _, groupID := range r.PeerGroups {
if g := nmd.Groups[groupID]; g != nil {
relevantGroupIDs[groupID] = g
}
}
for _, groupID := range r.Groups {
if g := nmd.Groups[groupID]; g != nil {
relevantGroupIDs[groupID] = g
}
}
if r.Enabled {
for _, groupID := range r.AccessControlGroups {
if g := nmd.Groups[groupID]; g != nil {
relevantGroupIDs[groupID] = g
}
routeAccessControlGroups[groupID] = struct{}{}
}
}
if r.Peer != "" {
if _, ok := nmd.ValidatedPeers[r.Peer]; ok {
if p := nmd.Peers[r.Peer]; p != nil {
relevantPeerIDs[r.Peer] = p
}
}
}
for _, groupID := range r.PeerGroups {
g := nmd.Groups[groupID]
if g == nil {
continue
}
for _, pid := range g.Peers {
if _, exists := relevantPeerIDs[pid]; exists {
continue
}
if _, ok := nmd.ValidatedPeers[pid]; !ok {
continue
}
if p := nmd.Peers[pid]; p != nil {
relevantPeerIDs[pid] = p
}
}
}
relevantRoutes = append(relevantRoutes, r)
}
for _, policy := range nmd.Policies {
if policy == nil || !policy.Enabled {
continue
}
policyRelevant := false
for i, rule := range policy.Rules {
if rule == nil || !rule.Enabled {
continue
}
if len(routeAccessControlGroups) > 0 {
for _, destGroupID := range rule.Destinations {
if _, needed := routeAccessControlGroups[destGroupID]; needed {
policyRelevant = true
for _, srcGroupID := range rule.Sources {
if g := nmd.Groups[srcGroupID]; g != nil {
relevantGroupIDs[srcGroupID] = g
}
}
for _, dstGroupID := range rule.Destinations {
if g := nmd.Groups[dstGroupID]; g != nil {
relevantGroupIDs[dstGroupID] = g
}
}
break
}
}
}
sources, destinations := nmd.ruleAdmissions(policy, i)
peerInSources := nmd.sideAdmitsPeer(rule.Sources, rule.SourceResource, policy.SourcePostureChecks, peerID, peerGroupSet)
peerInDestinations := nmd.sideAdmitsPeer(rule.Destinations, rule.DestinationResource, nil, peerID, peerGroupSet)
if peerInSources {
policyRelevant = true
for _, pid := range destinations.peers {
relevantPeerIDs[pid] = nmd.Peers[pid]
}
for _, dstGroupID := range rule.Destinations {
if g := nmd.Groups[dstGroupID]; g != nil {
relevantGroupIDs[dstGroupID] = g
}
}
}
if peerInDestinations {
policyRelevant = true
for _, pid := range sources.peers {
relevantPeerIDs[pid] = nmd.Peers[pid]
}
for _, srcGroupID := range rule.Sources {
if g := nmd.Groups[srcGroupID]; g != nil {
relevantGroupIDs[srcGroupID] = g
}
}
if rule.Protocol == string(types.PolicyRuleProtocolNetbirdSSH) {
switch {
case len(rule.AuthorizedGroups) > 0:
for groupID := range rule.AuthorizedGroups {
sshReqs.neededGroupIDs[groupID] = struct{}{}
}
case rule.AuthorizedUser != "":
default:
sshReqs.needAllowedUserIDs = true
}
} else if nmdata.PolicyRuleImpliesLegacySSH(rule) && peerSSHEnabled {
sshReqs.needAllowedUserIDs = true
}
}
}
if policyRelevant {
relevantPolicies = append(relevantPolicies, policy)
}
}
return relevantPeerIDs, relevantGroupIDs, relevantPolicies, relevantRoutes, sshReqs
}
// getPeersFromGroups returns the admitted members of a rule side given by groups.
// The All group, or a single group, alone decides the side. Posture failures are
// recorded in postureFailedPeers.
func (nmd *NetworkMapData) getPeersFromGroups(groups []string, sourcePostureChecksIDs []string,
postureFailedPeers *map[string]map[string]struct{}) []string {
filteredPeerIDs := make([]string, 0, len(groups))
seenPeerIds := make(map[string]struct{}, len(groups))
for _, gid := range groups {
group := nmd.Groups[gid]
if group == nil {
continue
}
if group.IsGroupAll() || len(groups) == 1 {
filteredPeerIDs = make([]string, 0, len(group.Peers))
for _, pid := range group.Peers {
if nmd.admitPolicyPeer(pid, sourcePostureChecksIDs, postureFailedPeers) {
filteredPeerIDs = append(filteredPeerIDs, pid)
}
}
return filteredPeerIDs
}
for _, pid := range group.Peers {
if _, seen := seenPeerIds[pid]; seen {
continue
}
seenPeerIds[pid] = struct{}{}
if nmd.admitPolicyPeer(pid, sourcePostureChecksIDs, postureFailedPeers) {
filteredPeerIDs = append(filteredPeerIDs, pid)
}
}
}
return filteredPeerIDs
}
// getPeerFromResource resolves a rule side that names a peer directly, admitting it
// like a member of a group holding only that peer.
func (nmd *NetworkMapData) getPeerFromResource(resource nmdata.Resource, sourcePostureChecksIDs []string,
postureFailedPeers *map[string]map[string]struct{}) []string {
if !nmd.admitPolicyPeer(resource.ID, sourcePostureChecksIDs, postureFailedPeers) {
return nil
}
return []string{resource.ID}
}
// admitPolicyPeer is admitsPeer that records a failed posture check in postureFailedPeers.
func (nmd *NetworkMapData) admitPolicyPeer(pid string, sourcePostureChecksIDs []string, postureFailedPeers *map[string]map[string]struct{}) bool {
admitted, failedCheck := nmd.admitsPeer(pid, sourcePostureChecksIDs)
if failedCheck != "" {
if _, ok := (*postureFailedPeers)[failedCheck]; !ok {
(*postureFailedPeers)[failedCheck] = make(map[string]struct{})
}
(*postureFailedPeers)[failedCheck][pid] = struct{}{}
}
return admitted
}
// admitsPeer applies the per-peer admission of a rule side: the peer must exist, be
// validated and pass the rule's posture checks. It returns the first failed posture
// check ID, if any.
func (nmd *NetworkMapData) admitsPeer(pid string, sourcePostureChecksIDs []string) (bool, string) {
peer, ok := nmd.Peers[pid]
if !ok || peer == nil {
return false, ""
}
if _, ok := nmd.ValidatedPeers[pid]; !ok {
return false, ""
}
isValid, pname := nmd.validatePostureChecksOnPeerGetFailed(sourcePostureChecksIDs, pid)
if !isValid && len(pname) > 0 {
return false, pname
}
return true, ""
}
func (nmd *NetworkMapData) validatePostureChecksOnPeerGetFailed(sourcePostureChecksID []string, peerID string) (bool, string) {
peer, ok := nmd.Peers[peerID]
if !ok || peer == nil {
return false, ""
}
for _, postureChecksID := range sourcePostureChecksID {
if valid, cached := nmd.postureVerdict(postureChecksID, peerID); cached {
if !valid {
return false, postureChecksID
}
continue
}
postureChecks := nmd.PostureChecks[postureChecksID]
if postureChecks == nil {
continue
}
if !postureChecks.Passes(peer) {
return false, postureChecksID
}
}
return true, ""
}
// PrecomputePostureValidation evaluates every posture check used by an enabled policy
// for the policy's source peers and stores the results in PostureValidation.
func (nmd *NetworkMapData) PrecomputePostureValidation() {
if len(nmd.PostureChecks) == 0 {
nmd.PostureValidation = nil
return
}
results := make(map[string]map[string]bool)
for _, policy := range nmd.Policies {
if policy == nil || !policy.Enabled {
continue
}
for _, postureChecksID := range policy.SourcePostureChecks {
if _, done := results[postureChecksID]; !done {
results[postureChecksID] = nmd.evaluatePostureChecksForPeers(postureChecksID, nmd.postureCheckPeerIDs(postureChecksID))
}
}
}
nmd.PostureValidation = results
}
func (nmd *NetworkMapData) evaluatePostureChecksForPeers(postureChecksID string, peerIDs map[string]struct{}) map[string]bool {
postureChecks := nmd.PostureChecks[postureChecksID]
if postureChecks == nil {
return nil
}
checks := postureChecks.GetChecks()
results := make(map[string]bool, len(peerIDs))
for peerID := range peerIDs {
peer := nmd.Peers[peerID]
if peer == nil {
continue
}
results[peerID] = nmdata.PassesChecks(checks, peer)
}
return results
}
// mergeSourcePeers adds the admitted source peers of a resource policy to the components.
func (nmd *NetworkMapData) mergeSourcePeers(components *types.NetworkMapComponents, sources *admittedPeers) {
for _, pID := range sources.peers {
if _, exists := components.Peers[pID]; !exists {
components.Peers[pID] = nmd.Peers[pID]
}
}
}
// collectPostureFailedPeers sets the components' posture failures to those of every
// enabled rule side, of the given resource-policy sources and the ones already
// recorded, limited to the checks the components' policies reference and to the peers
// in the components. Each failure set is intersected with the components' peers from
// its smaller side, so the cost follows the peer's own map, not the account size.
func (nmd *NetworkMapData) collectPostureFailedPeers(components *types.NetworkMapComponents, sourceFailures []map[string]map[string]struct{}) {
failures := append([]map[string]map[string]struct{}{nmd.ruleFailures(), components.PostureFailedPeers}, sourceFailures...)
referenced := make(map[string]struct{})
addReferenced := func(policy *nmdata.Policy) {
if policy == nil {
return
}
for _, checkID := range policy.SourcePostureChecks {
referenced[checkID] = struct{}{}
}
}
for _, policy := range components.Policies {
addReferenced(policy)
}
for _, policies := range components.ResourcePoliciesMap {
for _, policy := range policies {
addReferenced(policy)
}
}
result := make(map[string]map[string]struct{})
for checkID := range referenced {
var kept map[string]struct{}
for _, failed := range failures {
kept = addFailedPeersInMap(kept, failed[checkID], components.Peers)
}
if len(kept) > 0 {
result[checkID] = kept
}
}
components.PostureFailedPeers = result
}
// addFailedPeersInMap adds the failed peers that are in peers to kept, allocating kept
// on the first match, and returns it.
func addFailedPeersInMap(kept, failed map[string]struct{}, peers map[string]*nmdata.Peer) map[string]struct{} {
add := func(pid string) {
if kept == nil {
kept = make(map[string]struct{})
}
kept[pid] = struct{}{}
}
if len(failed) <= len(peers) {
for pid := range failed {
if _, ok := peers[pid]; ok {
add(pid)
}
}
return kept
}
for pid := range peers {
if _, ok := failed[pid]; ok {
add(pid)
}
}
return kept
}
// forcesRoutingPeerDNSResolution reports whether the given peer must run
// routing-peer DNS resolution regardless of the account-global
// RoutingPeerDNSResolutionEnabled setting: true when the peer routes a domain
// network resource targeted by an enabled reverse-proxy service, so the peer's
// DNS forwarder starts and can resolve the target for the embedded proxy peers.
func (nmd *NetworkMapData) forcesRoutingPeerDNSResolution(peerID string) bool {
if len(nmd.ProxyTargetedDomainResourceIDs) == 0 {
return false
}
for _, resource := range nmd.NetworkResources {
if resource == nil || !resource.Enabled || resource.Type != string(types.ResourceTypeDomain) {
continue
}
if _, ok := nmd.ProxyTargetedDomainResourceIDs[resource.ID]; !ok {
continue
}
if _, isRouter := nmd.Routers[resource.NetworkID][peerID]; isRouter {
return true
}
}
return false
}
// GetPeerGroups returns the set of group IDs the peer belongs to. The
// underlying peer→groups index is built once per NetworkMapData and the
// returned set is shared — callers must not mutate it.
func (nmd *NetworkMapData) GetPeerGroups(peerID string) map[string]struct{} {
nmd.peerGroupsOnce.Do(func() {
idx := make(map[string]map[string]struct{}, len(nmd.Peers))
for groupID, group := range nmd.Groups {
if group == nil {
continue
}
for _, pid := range group.Peers {
set, ok := idx[pid]
if !ok {
set = make(map[string]struct{})
idx[pid] = set
}
set[groupID] = struct{}{}
}
}
nmd.peerGroupsIdx = idx
})
if set, ok := nmd.peerGroupsIdx[peerID]; ok {
return set
}
return map[string]struct{}{}
}
func (nmd *NetworkMapData) getUniquePeerIDsFromGroupsIDs(groups []string) []string {
peerIDs := make(map[string]struct{}, len(groups))
for _, groupID := range groups {
group := nmd.Groups[groupID]
if group == nil {
continue
}
if group.IsGroupAll() || len(groups) == 1 {
return group.Peers
}
for _, peerID := range group.Peers {
peerIDs[peerID] = struct{}{}
}
}
ids := make([]string, 0, len(peerIDs))
for peerID := range peerIDs {
ids = append(ids, peerID)
}
return ids
}
func (nmd *NetworkMapData) getAllowedUserIDs() map[string]struct{} {
return nmd.AllowedUserIDs
}
func (nmd *NetworkMapData) appliedZones(peerGroups map[string]struct{}) []nmdata.CustomZone {
if len(peerGroups) == 0 {
return nil
}
var out []nmdata.CustomZone
for _, cand := range nmd.AppliedZoneCandidates {
if peerInDistributionGroups(peerGroups, cand.DistributionGroups) {
out = append(out, cand.Zone)
}
}
return out
}
func (nmd *NetworkMapData) privateServiceZones(peerGroups map[string]struct{}) []nmdata.CustomZone {
byApex := make(map[string]*nmdata.CustomZone)
var order []string
for _, cand := range nmd.PrivateServiceCandidates {
if !peerInDistributionGroups(peerGroups, cand.AccessGroups) {
continue
}
zone, exists := byApex[cand.Zone.Domain]
if !exists {
nz := nmdata.CustomZone{
Domain: cand.Zone.Domain,
SearchDomainDisabled: cand.Zone.SearchDomainDisabled,
NonAuthoritative: cand.Zone.NonAuthoritative,
}
byApex[cand.Zone.Domain] = &nz
zone = &nz
order = append(order, cand.Zone.Domain)
}
zone.Records = append(zone.Records, cand.Zone.Records...)
}
var out []nmdata.CustomZone
for _, apex := range order {
zone := byApex[apex]
if len(zone.Records) == 0 {
continue
}
out = append(out, *zone)
}
return out
}
func peerInDistributionGroups(peerGroups map[string]struct{}, groups []string) bool {
for _, g := range groups {
if _, ok := peerGroups[g]; ok {
return true
}
}
return false
}
func filterGroupPeers(groups *map[string]*nmdata.Group, peers map[string]*nmdata.Peer) {
for groupID, groupInfo := range *groups {
filteredPeers := make([]string, 0, len(groupInfo.Peers))
for _, pid := range groupInfo.Peers {
if _, exists := peers[pid]; exists {
filteredPeers = append(filteredPeers, pid)
}
}
if len(filteredPeers) != len(groupInfo.Peers) {
ng := groupInfo.Copy()
ng.Peers = filteredPeers
(*groups)[groupID] = ng
}
}
}
func filterDNSRecordsByPeers(records []nmdata.SimpleRecord, peers map[string]*nmdata.Peer, includeIPv6 bool) []nmdata.SimpleRecord {
if len(records) == 0 || len(peers) == 0 {
return nil
}
peerIPs := make(map[string]struct{}, len(peers)*2)
for _, peer := range peers {
if peer == nil {
continue
}
peerIPs[peer.IP.String()] = struct{}{}
if includeIPv6 && peer.IPv6.IsValid() {
peerIPs[peer.IPv6.String()] = struct{}{}
}
}
filteredRecords := make([]nmdata.SimpleRecord, 0, len(records))
for _, record := range records {
if _, exists := peerIPs[record.RData]; exists {
filteredRecords = append(filteredRecords, record)
}
}
return filteredRecords
}
func filterGroupIDToUserIDs(fullMap map[string][]string, neededGroupIDs map[string]struct{}) map[string][]string {
if len(neededGroupIDs) == 0 {
return nil
}
filtered := make(map[string][]string, len(neededGroupIDs))
for groupID := range neededGroupIDs {
if users, ok := fullMap[groupID]; ok {
filtered[groupID] = users
}
}
return filtered
}