Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Features #180

Merged
merged 6 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions server/config/bruteforce.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ package config
import "fmt"

type BruteForceSection struct {
IPWhitelist []string `mapstructure:"ip_whitelist"`
Buckets []BruteForceRule `mapstructure:"buckets"`
Learning []*Feature `mapstructure:"learning"`
SoftWhitelist `mapstructure:"soft_whitelist"`
IPWhitelist []string `mapstructure:"ip_whitelist"`
Buckets []BruteForceRule `mapstructure:"buckets"`
Learning []*Feature `mapstructure:"learning"`
}

func (b *BruteForceSection) String() string {
Expand Down
1 change: 1 addition & 0 deletions server/config/features.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package config
import "fmt"

type RelayDomainsSection struct {
SoftWhitelist `mapstructure:"soft_whitelist"`
StaticDomains []string `mapstructure:"static"`
}

Expand Down
7 changes: 4 additions & 3 deletions server/config/rbl.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ package config
import "fmt"

type RBLSection struct {
Lists []RBL
Threshold int
IPWhiteList []string `mapstructure:"ip_whitelist"`
SoftWhitelist `mapstructure:"soft_whitelist"`
Lists []RBL
Threshold int
IPWhiteList []string `mapstructure:"ip_whitelist"`
}

func (r *RBLSection) String() string {
Expand Down
145 changes: 145 additions & 0 deletions server/config/softallow.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package config

import (
"net"
"strings"
"sync"
)

var mu = &sync.RWMutex{}

// SoftWhitelistProvider defines the methods for managing a soft whitelist of networks associated with usernames.
// The interface allows checking the existence of a whitelist, retrieving, setting, and deleting networks.
type SoftWhitelistProvider interface {
// HasSoftWhitelist checks if there is at least one entry in the soft whitelist, returning true if it exists, otherwise false.
HasSoftWhitelist() bool

// Get retrieves the list of networks associated with the given username from the soft whitelist.
Get(username string) []string

// Set adds a specified network to a user's whitelist if the network is valid and the username is not empty.
Set(username, network string)

// Delete removes a specified network from the user's soft whitelist identified by the provided username.
Delete(username, network string)
}

// SoftWhitelist is a type that represents a map linking a string key to a slice of string values.
// Typically used to associate users with a list of CIDR networks.
type SoftWhitelist map[string][]string

// NewSoftWhitelist creates and returns a new instance of SoftWhitelist initialized as an empty map of string slices.
func NewSoftWhitelist() SoftWhitelist {
return make(SoftWhitelist)
}

func (s SoftWhitelist) String() string {
if s == nil {
return "SoftWhitelist: <nil>"
}

for k, v := range s {
return "SoftWhitelist: {SoftWhitelist[" + k + "]: " + strings.Join(v, ", ") + "}"
}

return "SoftWhitelist: {SoftWhitelist: <empty>}"
}

// HasSoftWhitelist checks if the SoftWhitelist is non-nil and contains at least one entry.
func (s SoftWhitelist) HasSoftWhitelist() bool {
if s == nil {
return false
}

mu.RLock()

defer mu.RUnlock()

return len(s) > 0
}

// isValidNetwork checks if the provided network string is a valid CIDR notation.
// It returns true if the network is valid, otherwise false.
func (s SoftWhitelist) isValidNetwork(network string) bool {
_, _, err := net.ParseCIDR(network)

return err == nil
}

// Set adds a specified network to a user's whitelist if the network is valid and the username is not empty.
func (s SoftWhitelist) Set(username, network string) {
if s == nil {
return
}

mu.Lock()

defer mu.Unlock()

if len(username) == 0 {
return
}

if s.isValidNetwork(network) {
if s[username] == nil {
s[username] = make([]string, 0)
}

s[username] = append(s[username], network)
}
}

// Get retrieves the list of networks associated with the specified username from the SoftWhitelist.
// If the SoftWhitelist is nil or the username does not exist, it returns nil.
func (s SoftWhitelist) Get(username string) []string {
if s == nil {
return nil
}

mu.RLock()

defer mu.RUnlock()

for k, v := range s {
if k == username {
return v
}
}

return nil
}

// Delete removes the specified network from the user's whitelist in the SoftWhitelist. If the network is the only entry,
// the user is removed from the whitelist. The function does nothing if the whitelist is nil or if the user does not exist.
func (s SoftWhitelist) Delete(username, network string) {
if s == nil {
return
}

mu.Lock()

defer mu.Unlock()

networks, exists := s[username]
if !exists {
return
}

if len(networks) > 1 {
for i, n := range networks {
if n == network {
networks = append(networks[:i], networks[i+1:]...)

break
}
}

s[username] = networks
} else {
if s[username][0] == network {
delete(s, username)
}
}
}

var _ SoftWhitelistProvider = (*SoftWhitelist)(nil)
196 changes: 196 additions & 0 deletions server/config/softallow_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
package config

import (
"testing"
)

func TestSoftWhitelist_String(t *testing.T) {
tests := []struct {
name string
s SoftWhitelist
want string
}{
{"NilSoftWhitelist", nil, "SoftWhitelist: <nil>"},
{"EmptySoftWhitelist", SoftWhitelist{}, "SoftWhitelist: {SoftWhitelist: <empty>}"},
{
"NonEmptySoftWhitelist",
SoftWhitelist{"user1": {"192.168.1.0/24"}},
"SoftWhitelist: {SoftWhitelist[user1]: 192.168.1.0/24}",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.s.String(); got != tt.want {
t.Errorf("String() = %v, want %v", got, tt.want)
}
})
}
}

func TestSoftWhitelist_HasSoftWhitelist(t *testing.T) {
tests := []struct {
name string
s SoftWhitelist
want bool
}{
{"NilSoftWhitelist", nil, false},
{"EmptySoftWhitelist", SoftWhitelist{}, false},
{"NonEmptySoftWhitelist", SoftWhitelist{"user1": {"192.168.1.0/24"}}, true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.s.HasSoftWhitelist(); got != tt.want {
t.Errorf("HasSoftWhitelist() = %v, want %v", got, tt.want)
}
})
}
}

func TestSoftWhitelist_isValidNetwork(t *testing.T) {
tests := []struct {
name string
s SoftWhitelist
network string
want bool
}{
{"ValidIPv4CIDR", SoftWhitelist{}, "192.168.1.0/24", true},
{"InvalidCIDR", SoftWhitelist{}, "192.168.1.0", false},
{"InvalidFormat", SoftWhitelist{}, "invalid", false},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.s.isValidNetwork(tt.network); got != tt.want {
t.Errorf("isValidNetwork() = %v, want %v", got, tt.want)
}
})
}
}

func TestSoftWhitelist_Set(t *testing.T) {
s := NewSoftWhitelist()
s.Set("user1", "192.168.1.0/24")

t.Run("SetValid", func(t *testing.T) {
if got := s.Get("user1"); len(got) != 1 || got[0] != "192.168.1.0/24" {
t.Errorf("Expected network to be added, got %v", got)
}
})

s.Set("user1", "10.0.0.0/8")
t.Run("SetAdditionalNetwork", func(t *testing.T) {
if got := s.Get("user1"); len(got) != 2 || got[1] != "10.0.0.0/8" {
t.Errorf("Expected additional network to be added, got %v", got)
}
})

s.Set("", "10.0.0.0/8")
t.Run("SetEmptyUsername", func(t *testing.T) {
if got := s.Get(""); got != nil {
t.Errorf("Expected no networks for empty username, got %v", got)
}
})

s.Set("user2", "invalid")
t.Run("SetInvalidNetwork", func(t *testing.T) {
if got := s.Get("user2"); got != nil {
t.Errorf("Expected no networks for invalid network, got %v", got)
}
})
}

func TestSoftWhitelist_Get(t *testing.T) {
s := NewSoftWhitelist()
s.Set("user1", "192.168.1.0/24")

tests := []struct {
name string
s SoftWhitelist
username string
want []string
}{
{"GetExistingUser", s, "user1", []string{"192.168.1.0/24"}},
{"GetNonExistingUser", s, "user2", nil},
{"GetFromNilWhitelist", nil, "user1", nil},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.s.Get(tt.username); !equalSlices(got, tt.want) {
t.Errorf("Get() = %v, want %v", got, tt.want)
}
})
}
}

func TestNewSoftAllow(t *testing.T) {
t.Run("NewSoftWhitelist", func(t *testing.T) {
if got := NewSoftWhitelist(); got == nil || len(got) != 0 {
t.Errorf("NewSoftWhitelist() = %v, want empty SoftWhitelist", got)
}
})
}

func equalSlices(a, b []string) bool {
if len(a) != len(b) {
return false
}

for i := range a {
if a[i] != b[i] {
return false
}
}

return true
}

func TestSoftWhitelist_Delete(t *testing.T) {
tests := []struct {
name string
s SoftWhitelist
username string
network string
want map[string][]string
}{
{"DeleteFromNilWhitelist", nil, "user1", "192.168.1.0/24", nil},
{"DeleteFromEmptyWhitelist", NewSoftWhitelist(), "user1", "192.168.1.0/24", map[string][]string{}},
{"DeleteNonExistentNetwork", SoftWhitelist{"user1": {"192.168.1.0/24"}}, "user1", "10.0.0.0/8", SoftWhitelist{"user1": {"192.168.1.0/24"}}},
{"DeleteExistingNetwork", SoftWhitelist{"user1": {"192.168.1.0/24", "10.0.0.0/8"}}, "user1", "192.168.1.0/24", SoftWhitelist{"user1": {"10.0.0.0/8"}}},
{"DeleteOnlyNetwork", SoftWhitelist{"user1": {"192.168.1.0/24"}}, "user1", "192.168.1.0/24", map[string][]string{}},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.s.Delete(tt.username, tt.network)
if !equalMaps(tt.s, tt.want) {
t.Errorf("Delete() result = %v, want %v", tt.s, tt.want)
}
})
}
}

func equalMaps(a, b SoftWhitelist) bool {
if a == nil && b == nil {
return true
}

if a == nil || b == nil {
return false
}

if len(a) != len(b) {
return false
}

for k, v := range a {
bv, ok := b[k]
if !ok || !equalSlices(v, bv) {
return false
}
}

return true
}
Loading
Loading