Skip to content

Commit

Permalink
Merge pull request #180 from croessner/features
Browse files Browse the repository at this point in the history
Features
  • Loading branch information
croessner authored Dec 3, 2024
2 parents a2baf4d + 136e6b5 commit 8bdd3db
Show file tree
Hide file tree
Showing 12 changed files with 736 additions and 159 deletions.
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

0 comments on commit 8bdd3db

Please sign in to comment.