Add end-to-end tests to Bottin #2

Merged
erwan merged 3 commits from test-go-Bottin into main 2021-07-22 07:59:11 +00:00
17 changed files with 840 additions and 490 deletions
Showing only changes of commit 9a8c19ec0f - Show all commits

View file

@ -8,19 +8,18 @@ steps:
commands:
- go get -d -v
- go build -v
- cd test_automatic
- go get -d -v
- go build -v
- cd test
- go test -i -c -o test
- name: test_bottin
image: consul:latest
environment:
BOTTIN_DEFAULT_ADMIN_PW: priZ4Cg0x5NkSyiIN/MpvWw4ZEy8f8s1
commands:
- ash test_automatic/start_test.sh
- ash test/runner.sh
---
kind: signature
hmac: a4455c124ee87ca8b0ef1779560703573f3a3f24d406e4cb281b9e0dab4ceeda
hmac: ff246a04c3df8a2f39c8b446dea920622d61950e6caaac886931bdb05d0706ed
...

2
.gitignore vendored
View file

@ -1,4 +1,4 @@
bottin
bottin.static
config.json
test_automatic/integration
test/test

View file

@ -330,7 +330,7 @@ func (server *Server) init() error {
}
admin_pass_str = base64.RawURLEncoding.EncodeToString(admin_pass)
} else {
server.logger.Printf("It seems that exists a password in environnement variable")
server.logger.Debug("BOTTIN_DEFAULT_ADMIN_PW environment variable is set, using it for admin's password")
erwan marked this conversation as resolved Outdated

Je mettrais bien quelque chose comme ça :

logger.Debug("BOTTIN_DEFAULT_ADMIN_PW environment variable is set, using it for admin's password")

Je pense qu'on devrait utiliser le logger "normal" ici, du moins si on peut.

Je mettrais bien quelque chose comme ça : ```go logger.Debug("BOTTIN_DEFAULT_ADMIN_PW environment variable is set, using it for admin's password") ``` Je pense qu'on devrait utiliser le logger "normal" ici, du moins si on peut.
}
admin_pass_hash := SSHAEncode([]byte(admin_pass_str))

160
test/bottin_test.go Normal file
View file

@ -0,0 +1,160 @@
package main
import (
"testing"
)
func TestAddThenDelete(t *testing.T) {
t.Parallel()
//SetUp - Create Users and Groups
inst, err := Init()
if err != nil {
t.Error(err)
}
//TearDown - Delete all the users and groups created
err = inst.Clean()
if err != nil {
t.Error(err)
}
}
func TestConfirmAddAttributes(t *testing.T) {
t.Parallel()
//SetUp - Create Users and Groups
inst, err := Init()
if err != nil {
t.Error(err)
}
//Test search_attribute to confirm the Add
if ok, err := inst.CompareOurDataWithConsul(); !ok {
t.Error(err)
}
//TearDown - Delete all the users and groups created
err = inst.Clean()
if err != nil {
t.Error(err)
}
}
//Modifyrequest Test
func TestModifyRequest(t *testing.T) {
t.Parallel()
//SetUp - Create Users and Groups
inst, err := Init()
if err != nil {
t.Error(err)
}
//Test modify all data (groups and users)
err = inst.ModifyRandomAllData()
if err != nil {
t.Error(err)
}
//TearDown - Delete all the users and groups created
err = inst.Clean()
if err != nil {
t.Error(err)
}
}
func TestModifyRequestAndCheck(t *testing.T) {
t.Parallel()
//SetUp - Create Users and Groups
inst, err := Init()
if err != nil {
t.Error(err)
}
//Test modify all data (groups and users)
err = inst.ModifyRandomAllData()
if err != nil {
t.Error(err)
}
//Check if the data was modify on Consul
if ok, err := inst.CompareOurDataWithConsul(); !ok {
t.Error(err)
}
//TearDown - Delete all the users and groups created
err = inst.Clean()
if err != nil {
t.Error(err)
}
}
func TestAddUserInGroup(t *testing.T) {
t.Parallel()
//SetUp - Create Users and Groups
inst, err := Init()
if err != nil {
t.Error(err)
}
//Add users in group
err = inst.AddAllUsersInGroup()
if err != nil {
t.Error(err)
}
//TearDown - Delete all the users and groups created
err = inst.Clean()
if err != nil {
t.Error(err)
}
}
func TestDeleteGroupsAfterAddedUsers(t *testing.T) {
t.Parallel()
//SetUp - Create Users and Groups
inst, err := Init()
if err != nil {
t.Error(err)
}
//Add users in group
err = inst.AddAllUsersInGroup()
if err != nil {
t.Error(err)
}
//Delete the half groups
number := len(inst.dataGroups) / 2
err = inst.clean(inst.dataGroups[0:number])
if err != nil {
t.Error(err)
}
inst.dataGroups = inst.dataGroups[number:len(inst.dataGroups)]
//Check all the groups in memberOf exist
ok, err := inst.CheckMemberOf()
if err != nil {
t.Error(err)
}
if !ok {
t.Errorf("Found group in memberOf that isn't in Consul.")
}
//TearDown - Delete all the users and groups created
err = inst.Clean()
if err != nil {
t.Error(err)
}
}
//Example of paralellism Test
func TestPrincipal(t *testing.T) {
t.Run("A=1", TestAddThenDelete)
t.Run("A=2", TestModifyRequest)
if !testing.Short() {
t.Run("B=1", TestConfirmAddAttributes)
t.Run("B=2", TestModifyRequestAndCheck)
t.Run("C=1", TestAddUserInGroup)
t.Run("C=2", TestDeleteGroupsAfterAddedUsers)
}
}

281
test/create.go Normal file
View file

@ -0,0 +1,281 @@
package main
import (
"fmt"
"strings"
"sync"
"github.com/go-ldap/ldap/v3"
"github.com/sirupsen/logrus"
)
//Mux value, this value permits do not have two identicals values in the parallel instances
type StoreAllCN struct {
mu sync.Mutex
cn map[string]struct{}
}
var allNames = StoreAllCN{cn: make(map[string]struct{})}
//Type used for the tests
type attributes struct {
Name string
Data []string
}
type data_DN struct {
DN string
Attributes []attributes
}
type instance struct {
numberUsers, numberGroups int
dataGroups, dataUsers []data_DN
logging *ldap.Conn
}
//Create a new object instance
//With this instance, we can obtain an isolated container where
//we have our users and groups. It allows to run tests in parallel.
func NewInstance(numberUsers, numberGroups int) (*instance, error) {
l, err := Connect()
if err != nil {
return nil, err
}
logging.Level = logrus.InfoLevel
inst := instance{
numberUsers: numberUsers,
numberGroups: numberGroups,
dataGroups: []data_DN{},
dataUsers: []data_DN{},
logging: l,
}
err = inst.createOrganizationnalUnit()
if ldap.IsErrorWithCode(err, uint16(68)) {
logging.Warn("OrganizationnalUnit already created")
err = nil
}
if err != nil {
return nil, err
}
err = inst.CreateGroups()
if err != nil {
return nil, err
}
err = inst.CreateUsers()
if err != nil {
return nil, err
}
return &inst, nil
}
//Part: Created users or groups or OU
func (inst *instance) createOrganizationnalUnit() error {
dn := []string{"ou=groups,dc=deuxfleurs,dc=fr", "ou=users,dc=deuxfleurs,dc=fr"}
attributes := []map[string][]string{{
"description": []string{"OrganizationalUnit qui regroupe tous les groupes"},
"objectclass": []string{"organizationalUnit", "top"},
"ou": []string{"groups"},
"structuralobjectclass": []string{"organizationalUnit"},
},
{
"description": []string{"OrganizationalUnit qui regroupe tous les users"},
"objectclass": []string{"organizationalUnit", "top"},
"ou": []string{"users"},
"structuralobjectclass": []string{"organizationalUnit"},
},
}
for index := range dn {
err := inst.Add_Request(dn[index], attributes[index])
if err != nil {
return err
}
}
return nil
}
//Part: Create User or group
func (inst *instance) CreateUsers() (err error) {
dn := "cn=%s,ou=users,dc=deuxfleurs,dc=fr"
attributes := map[string][]string{
"displayname": {},
"objectclass": {"inetOrgPerson", "organizationalPerson", "person", "top"},
"structuralobjectclass": {"inetOrgPerson"},
}
du, err := inst.create(dn, []string{"displayname"}, inst.numberUsers, attributes, inst.dataUsers)
if err == nil {
inst.dataUsers = du
}
return err
}
func (inst *instance) CreateGroups() error {
dn := "cn=%s,ou=groups,dc=deuxfleurs,dc=fr"
attributes := map[string][]string{
"description": {},
"objectclass": {"groupOfNames", "top"},
"structuralobjectclass": {"groupOfNames"},
}
dg, err := inst.create(dn, []string{"description"}, inst.numberGroups, attributes, inst.dataGroups)
if err == nil {
inst.dataGroups = dg
}
return err
}
//Hard Function: She does:
//- generate an unique name
//- store the Data of each AddRequest in instance struct
//- send AddRequest to Bottin
func (inst *instance) create(dn string, unique_attr []string, number int, attributes map[string][]string, data []data_DN) ([]data_DN, error) {
for i := 0; i < number; i++ {
name := inst.GenerateName()
datDn := data_DN{DN: fmt.Sprintf(dn, name)}
for _, value := range unique_attr {
attributes[value] = []string{name}
}
datDn.Attributes = MapAttToStruct(attributes)
data = append(data, datDn)
err := inst.Add_Request(fmt.Sprintf(dn, name), attributes)
if err != nil {
return nil, err
}
}
return data, nil
}
//Part: clean
func (inst *instance) Clean() error {
err := inst.CleanGroups()
if err != nil {
return err
}
err = inst.CleanUsers()
return err
}
func (inst *instance) CleanUsers() error {
err := inst.clean(inst.dataUsers)
if err != nil {
return err
}
inst.dataUsers = []data_DN{}
return err
}
func (inst *instance) CleanGroups() error {
err := inst.clean(inst.dataGroups)
if err != nil {
return err
}
inst.dataGroups = []data_DN{}
return err
}
func (inst *instance) clean(stock []data_DN) error {
logging.Debugf("Delete %d elements.", len(stock))
for _, value := range stock {
err := inst.Delete_Request(value.DN)
if err != nil {
return err
}
}
return nil
}
//Part: Verify if a data_Dn is a group or an user
func (inst *instance) VerifyUser(user data_DN) (bool, error) {
dn := "ou=users,dc=deuxfleurs,dc=fr"
cn := strings.Split(user.DN, ",")[0]
filter := fmt.Sprintf("(%s)", cn)
res, err := inst.Search_Request(dn, filter, []string{"cn"})
return len(res.Entries) == 1, err
}
func (inst *instance) VerifyGroup(group data_DN) (bool, error) {
dn := "ou=groups,dc=deuxfleurs,dc=fr"
cn := strings.Split(group.DN, ",")[0]
filter := fmt.Sprintf("(%s)", cn)
res, err := inst.Search_Request(dn, filter, []string{"cn"})
return len(res.Entries) == 1, err
}
//Part: Add user in a group
func (inst *instance) AddUserInGroup(user, group data_DN) error {
err := inst.Modify_Request(group.DN, nil, nil, map[string][]string{
"member": {user.DN},
})
return err
}
func (inst *instance) AddUserSliceInGroup(users_cn []string, group_dn string) error {
err := inst.Modify_Request(group_dn, nil, nil, map[string][]string{
"member": users_cn,
})
return err
}
//Part: modify, add, delete data_DN struct
func AddAtt(name string, data []string, dat data_DN) data_DN {
dat.Attributes = append(dat.Attributes, attributes{
Name: name,
Data: data,
})
logging.Debug(fmt.Sprintf("Attributes %s add from %s.", name, dat.DN))
return dat
}
func DelAtt(name string, dat data_DN) data_DN {
for index, value := range dat.Attributes {
if value.Name == name {
dat.Attributes[index] = dat.Attributes[len(dat.Attributes)-1]
//tmp := dat.Attributes[:len(dat.Attributes)-1]
dat.Attributes = []attributes{}
logging.Debugf("Attributes %s delete from %s.", name, dat.DN)
return dat
}
}
logging.Debugf("Can't delete attribute %s from %s.", name, dat.DN)
return dat
}
func ReplaceAtt(name string, data []string, dat data_DN) data_DN {
for index, value := range dat.Attributes {
if value.Name == name {
dat.Attributes[index] = attributes{
Name: name,
Data: data,
}
logging.Debugf("Replace attributes %s from %s succesful..", name, dat.DN)
return dat
}
}
logging.Debugf("Can't replace attributes %s from %s.", name, dat.DN)
return dat
}

173
test/functionTest.go Normal file
View file

@ -0,0 +1,173 @@
package main
import (
"fmt"
"strings"
"github.com/go-ldap/ldap/v3"
)
const default_users, default_groups = 1000, 1000
func Init() (*instance, error) {
inst, err := NewInstance(default_users, default_groups)
return inst, err
}
//Part to compare our datas
func (inst *instance) CompareOurDataWithConsul() (bool, error) {
if ok, err := inst.VerifyOurData(inst.dataUsers); !ok {
return false, err
}
if ok, err := inst.VerifyOurData(inst.dataGroups); !ok {
return false, err
}
return true, nil
}
func (inst *instance) VerifyOurData(tabData []data_DN) (bool, error) {
for _, value := range tabData {
names := getNamesAtt(value)
cn := strings.Split(value.DN, ",")[0]
res, err := inst.Search_Request(value.DN, fmt.Sprintf("(&(%s))", cn), names)
if err != nil {
return false, err
}
if len(res.Entries) != 1 {
return false, fmt.Errorf("expected 1 entry, but found %d entry/ies", len(res.Entries))
}
if !Compare(value, res.Entries[0]) {
return false, fmt.Errorf("no match with the DN: %s", value.DN)
}
}
return true, nil
}
func Compare(dat data_DN, ent *ldap.Entry) bool {
for _, value := range dat.Attributes {
logging.Debugf("Attributes from %s is now: %s.", dat.DN, dat.Attributes)
entVal := GetAttributeValuesBottin(ent, value.Name)
logging.Debugf("Values of the Entry: attributName: %s, Values: %s.", value.Name, entVal)
if !CompareSliceString(entVal, value.Data) {
logging.Debugf("Values expected: %s, values found: %s.", value.Data, entVal)
return false
}
}
return true
}
//Part modify datas
func (inst *instance) ModifyRandomAllData() error {
dg, err := inst.ModifyRandom(inst.dataGroups, []string{"description"})
if err != nil {
return err
} else {
inst.dataGroups = dg
}
dg, err = inst.ModifyRandom(inst.dataUsers, []string{"displayname"})
if err != nil {
return err
} else {
inst.dataUsers = dg
}
return nil
}
//Function which modify random way the attributes in attName of a data_DN's slice, it can delete, replace and delete
//The function modify also in the dat object
func (inst *instance) ModifyRandom(dat []data_DN, attName []string) ([]data_DN, error) {
for index, value := range dat {
del := make(map[string][]string)
add := make(map[string][]string)
replace := make(map[string][]string)
for _, att := range attName {
switch selNumber := R.Intn(3); selNumber {
case 0:
del[att] = []string{}
value = DelAtt(att, value)
logging.Debug(fmt.Sprintf("Delete the attribute %s of the DN %s.", att, value.DN))
case 1:
name := inst.GenerateName()
value = AddAtt(name, []string{name}, value)
add[name] = []string{name}
logging.Debug(fmt.Sprintf("Add the attribute %s with value %s of the DN %s.", name, name, value.DN))
case 2:
name := inst.GenerateName()
value = ReplaceAtt(att, []string{name}, value)
replace[att] = []string{name}
logging.Debug(fmt.Sprintf("Replace the attribute %s with value %s of the DN %s.", att, name, value.DN))
}
}
err := inst.Modify_Request(value.DN, add, del, replace)
if err != nil {
return dat, err
}
dat[index] = value
}
return dat, nil
}
//Add all users in a random group
func (inst *instance) AddAllUsersInGroup() error {
for _, value := range inst.dataGroups {
valueRand := (len(inst.dataUsers) + 1) / 30
if valueRand == 0 {
valueRand = 1
}
numberOfMembers := R.Intn(valueRand) + 1
logging.Debugf("%s will be have %d members.", value.DN, numberOfMembers)
groupMemory := make(map[int]struct{})
users_cn := []string{}
for i := 0; i < numberOfMembers; i++ {
selectGroup := R.Intn(len(inst.dataUsers))
for _, ok := groupMemory[selectGroup]; ok; _, ok = groupMemory[selectGroup] {
selectGroup = R.Intn(len(inst.dataUsers))
logging.Debugf("Search an other member. The value is %d , and we have %d members available.", selectGroup, len(inst.dataUsers))
}
groupMemory[selectGroup] = struct{}{}
users_cn = append(users_cn, inst.dataGroups[selectGroup].DN)
}
err := inst.AddUserSliceInGroup(users_cn, value.DN)
if err != nil {
return err
}
}
return nil
}
//Check if the groups in memberOf exist in Consul
func (inst *instance) CheckMemberOf() (bool, error) {
for _, value := range inst.dataUsers {
cn := strings.Split(value.DN, ",")[0]
res, err := inst.Search_Request(value.DN, fmt.Sprintf("(&(%s))", cn), []string{"memberOf"})
if err != nil {
return false, err
}
if len(res.Entries) != 1 {
return false, fmt.Errorf("expected 1 entry, but found %d entry/ies", len(res.Entries))
}
attValues := GetAttributeValuesBottin(res.Entries[0], "memberOf")
for _, dnGroup := range attValues {
logging.Debugf("Verify if the group %s exist...", dnGroup)
ok, err := inst.VerifyGroup(data_DN{DN: dnGroup})
if err != nil {
return false, err
}
if !ok {
return false, fmt.Errorf("don't found the group: %s", dnGroup)
}
}
}
return true, nil
}

View file

@ -1,5 +1,6 @@
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c h1:/IBSNwUN8+eKzUzbJPqhK839ygXJ82sde8x3ogr6R28=
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-asn1-ber/asn1-ber v1.5.1 h1:pDbRAunXzIUXfx4CB2QJFv5IuPiuoW+sWvr/Us009o8=
github.com/go-asn1-ber/asn1-ber v1.5.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
@ -7,10 +8,12 @@ github.com/go-ldap/ldap/v3 v3.3.0 h1:lwx+SJpgOHd8tG6SumBQZXCmNX51zM8B1cfxJ5gv4tQ
github.com/go-ldap/ldap/v3 v3.3.0/go.mod h1:iYS1MdmrmceOJ1QOTnRXrIs7i3kloqtmGQjRvjKpyMg=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 h1:vEg9joUBmeBcK9iSJftGNf3coIG4HqZElCPehJsfAYM=

138
test/handler.go Normal file
View file

@ -0,0 +1,138 @@
package main
import (
"fmt"
"math/rand"
"os"
ldap "github.com/go-ldap/ldap/v3"
"github.com/sirupsen/logrus"
)
const maxlength_generateName, minlength_generateName = 25, 3
const bindusername = "cn=admin,dc=deuxfleurs,dc=fr"
const adresse = "127.0.0.1"
const port = 1389
var logging = logrus.New()
const seed = 654258
var R = rand.New(rand.NewSource(seed))
var bindpassword = "sf7yO52NCuE"
//Handler just to facilite the print error
func PrintError(LDAPError error) {
if LDAPError != nil {
logging.Fatal(LDAPError)
}
}
//Generate an unique name, which store in all_names
func (inst *instance) GenerateName() (name string) {
alphabet := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
length := R.Intn(maxlength_generateName) + minlength_generateName
//Check if this name not exist already
//Lock thhis variable because she is hared with other goroutine
allNames.mu.Lock()
for only_one := true; only_one; _, only_one = allNames.cn[name] {
//Create the name
for i := 0; i < length; i++ {
name += string(alphabet[R.Intn(len(alphabet))])
}
}
//Add the new name in the map to store this one
allNames.cn[name] = struct{}{}
allNames.mu.Unlock()
logging.Debug(fmt.Sprintf("Name generated: %s.", name))
return
}
//Handler to around the bug with MessageId
func (inst *instance) Reconnect() (err error) {
inst.logging.Close()
inst.logging, err = ldap.Dial("tcp", fmt.Sprintf("%s:%d", adresse, port))
if err != nil {
return
}
err = inst.logging.Bind(bindusername, bindpassword)
//logging.Debug("Reconnect succesful")
return
}
//Transform attributes in map format to the struct attributes
func MapAttToStruct(att map[string][]string) []attributes {
resultat := []attributes{}
for key, value := range att {
logging.Debug(fmt.Sprintf("Transform: key: %s, values: %s to attributes struct.\n", key, value))
resultat = append(resultat, attributes{
Name: key,
Data: value,
})
}
return resultat
}
func Connect() (*ldap.Conn, error) {
l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", adresse, port))
if err != nil {
return nil, err
}
if key, ok := os.LookupEnv("BOTTIN_DEFAULT_ADMIN_PW"); ok {
bindpassword = key
}
err = l.Bind(bindusername, bindpassword)
logging.Debug("Connection succesful")
return l, err
}
//Handler to get only attributes names
func getNamesAtt(dat data_DN) []string {
resultat := []string{}
for _, values := range dat.Attributes {
resultat = append(resultat, values.Name)
}
return resultat
}
//Handler to compare slice string
func CompareSliceString(string1, string2 []string) bool {
if len(string1) != len(string2) {
return false
} else {
for index := range string1 {
if string1[index] != string2[index] {
return false
}
}
}
return true
}
//Handler to remove an element in slice string
func DeleteElementSliceString(s string, sSlice []string) []string {
for index, value := range sSlice {
if value == s {
sSlice[index] = sSlice[len(sSlice)-1]
return sSlice[:len(sSlice)-1]
}
}
return sSlice
}
//Get attributes entry values bottin bug
func GetAttributeValuesBottin(ent *ldap.Entry, name string) (res []string) {
for _, val := range ent.Attributes {
if val.Name == name {
res = append(res, val.Values...)
}
}
return
}

67
test/request.go Normal file
View file

@ -0,0 +1,67 @@
package main
import (
ldap "github.com/go-ldap/ldap/v3"
)
func (inst *instance) Add_Request(dn string, attributes map[string][]string) error {
//Create the AddRequest
req := ldap.NewAddRequest(dn, nil)
for key, value := range attributes {
req.Attribute(key, value)
}
//Send the request
err := inst.logging.Add(req)
//@FIXME: Remove when you try to correct the bug MessageID
inst.Reconnect()
return err
}
//Use enum to select Replace,Delete,Modify
func (inst *instance) Modify_Request(dn string, add_attributes, delete_attributes, replace_attributes map[string][]string) error {
modifyReq := ldap.NewModifyRequest(dn, nil)
for key, value := range add_attributes {
modifyReq.Add(key, value)
}
for key, value := range delete_attributes {
modifyReq.Delete(key, value)
}
for key, value := range replace_attributes {
modifyReq.Replace(key, value)
}
err := inst.logging.Modify(modifyReq)
//@FIXME: Remove when you try to correct the bug MessageID
inst.Reconnect()
return err
}
func (inst *instance) Delete_Request(dn string) error {
del := ldap.NewDelRequest(dn, nil)
err := inst.logging.Del(del)
//@FIXME: Remove when you try to correct the bug MessageID
inst.Reconnect()
return err
}
func (inst *instance) Search_Request(dn, filter string, name_attributes []string) (*ldap.SearchResult, error) {
searchReq := ldap.NewSearchRequest(
dn,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
filter,
name_attributes,
nil,
)
res, err := inst.logging.Search(searchReq)
logging.Debugf("Search Request made with: dn: %s, filter: %s, attributes: %s. \n", dn, filter, name_attributes)
//@FIXME: Remove when you try to correct the bug MessageID
inst.Reconnect()
return res, err
}

12
test/runner.sh Executable file
View file

@ -0,0 +1,12 @@
#!/bin/sh
set -ex
echo $BOTTIN_DEFAULT_ADMIN_PW
consul agent -dev > /dev/null 2>&1 &
sleep 2
cp test/config.json.test config.json
./bottin > /dev/null 2>&1 &
sleep 1
./test/test -test.v -test.failfast -test.short -test.run TestPrincipal
./test/test -test.v -test.failfast -test.run TestPrincipal/B=

View file

@ -1,431 +0,0 @@
package main
import (
"github.com/go-ldap/ldap/v3"
"fmt"
log "github.com/sirupsen/logrus"
"math/rand"
"strings"
"errors"
"os"
)
const bindusername = "cn=admin,dc=deuxfleurs,dc=fr"
const adresse = "127.0.0.1"
const port = 1389
var bindpassword string
var all_names = make(map[string]struct{})
func printError(LDAPError error) {
if LDAPError != nil {
log.Fatal(LDAPError)
}
}
func createOU(l *ldap.Conn) error {
req := ldap.NewAddRequest("ou=groups,dc=deuxfleurs,dc=fr",nil)
req.Attribute("description",[]string{"OrganizationalUnit qui regroupe tous les groupes"})
req.Attribute("objectclass",[]string{"organizationalUnit", "top"})
req.Attribute("ou",[]string{"groups"})
req.Attribute("structuralobjectclass", []string{"organizationalUnit"})
err := l.Add(req)
if err != nil {
return err
}
req = ldap.NewAddRequest("ou=users,dc=deuxfleurs,dc=fr",nil)
req.Attribute("description",[]string{"OrganizationalUnit qui regroupe tous les utilisateurs"})
req.Attribute("objectclass",[]string{"organizationalUnit", "top"})
req.Attribute("ou",[]string{"users"})
req.Attribute("structuralobjectclass", []string{"organizationalUnit"})
err = l.Add(req)
return err
}
func generateName(r *rand.Rand) (name string) {
for only_one := true; only_one; _, only_one = all_names[name]{
name = fmt.Sprintf("%d",r.Int())
}
all_names[name] = struct{}{}
log.Debug(fmt.Sprintf("Name generated: %s.\n", name))
return
}
func createGroup(r *rand.Rand, l *ldap.Conn) (tab_AddRequest []ldap.AddRequest, err error) {
StructuralObjectClass := []string{"groupOfNames"}
ObjectClass := []string{"groupOfNames","top"}
for i := 0; i<20; i++ {
//Generate name and check if he is unique
name := generateName(r)
req := ldap.NewAddRequest(fmt.Sprintf("cn=%s,ou=groups,dc=deuxfleurs,dc=fr",name),nil)
req.Attribute("description",[]string{generateName(r)})
req.Attribute("objectclass",ObjectClass)
req.Attribute("structuralobjectclass",StructuralObjectClass)
err = l.Add(req)
if err != nil {
log.Warn(fmt.Sprintf("Erreur survenue sur la création du [%d] groupe.\n",i))
return nil, err
}
tab_AddRequest = append(tab_AddRequest, *req)
}
return
}
func createUser(r *rand.Rand, l *ldap.Conn) (tab_AddRequest []ldap.AddRequest, err error) {
StructuralObjectClass := []string{"inetOrgPerson"}
ObjectClass := []string{"inetOrgPerson","organizationalPerson","person","top"}
for i := 0; i<20; i++ {
name := generateName(r)
req := ldap.NewAddRequest(fmt.Sprintf("cn=%s,ou=users,dc=deuxfleurs,dc=fr",name),nil)
req.Attribute("displayname",[]string{generateName(r)})
req.Attribute("objectclass",ObjectClass)
req.Attribute("structuralobjectclass",StructuralObjectClass)
err = l.Add(req)
if err != nil {
log.Warn(fmt.Sprintf("Erreur survenue sur la création du [%d] user.\n",i))
return nil, err
}
tab_AddRequest = append(tab_AddRequest, *req)
}
return
}
func search_attributes(tab_Attributes []ldap.Attribute, tipe string) (*ldap.Attribute) {
for _,att := range tab_Attributes {
if att.Type == tipe {
return &att
}
}
return nil
// return ldap.Attribute{}
}
func test_attributes(l *ldap.Conn, tab_AddRequest []ldap.AddRequest, filter_objectclass, user_or_group string) (err error) {
for _, addRequest := range tab_AddRequest {
//On prend le cn en supposant qu'il est unique
cn := strings.Split(addRequest.DN,",")[0]
//On crée la requête pour la recherche
search_req := ldap.NewSearchRequest(
fmt.Sprintf("ou=%s,dc=deuxfleurs,dc=fr",user_or_group),
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
fmt.Sprintf("(&(objectclass=%s)(%s))", filter_objectclass,cn),
[]string{"displayname","objectclass","structuralobjectclass"},
nil,
)
//On lance la recherche
result, err := l.Search(search_req)
if err != nil {
return err
}
if len(result.Entries) != 1 {
return errors.New("Test a trouvé plusieurs displaynames en commun ou en a trouvé aucun")
}
//On compare les attributs qu'on a reçu avec les attributs qu'on a envoyé
result_attributes := result.Entries[0].Attributes
log.Debug(fmt.Sprintf("La longueur est de %d, contient : \n %s.\n",len(result_attributes), result_attributes))
//Notre recherche crée un attribut par valeur, même si les valeurs viennent du même nom d'attribut
//Par exemple: objectclass possède 4 valeurs. Alors on aura 4 EntryAttribute qui contient chacune une des 4 valeurs de l'attribut objectclass
//j est l'indice qui représente la j-ème valeur de notre attribut
var j int
var att *ldap.Attribute
for i,attributes := range result_attributes {
//On cherche l'attribut de l'user i qui a le même nom que celui qu'on a reçu et qu'on traite dans cette boucle
if j == 0 {
att = search_attributes(addRequest.Attributes, attributes.Name)
if att == nil {
return errors.New(fmt.Sprintf("Error: test_attributes - Don't find match name attributes. We search %s.\n", attributes.Name))
}
}
log.Debug(fmt.Sprintf("Le nom de l'attribut est %s, sa valeur est: \n %s.", att.Type, att))
if j >= len(att.Vals) || att.Vals[j] != attributes.Values[0] {
return errors.New(fmt.Sprintf("Error: test_attributes - Theses values aren't the same: %d, %d",att.Vals, attributes.Values))
}
if i+1 < len(result_attributes) && result_attributes[i+1].Name == attributes.Name {
j += 1
} else { j = 0}
}
}
return nil
}
func clean(l *ldap.Conn, AddReq_users, AddReq_groups []ldap.AddRequest,user, group bool) (err error){
log.Debug("Debut clean")
if(user) {
for _,req := range AddReq_users {
delReq := ldap.NewDelRequest(req.DN,nil)
err = l.Del(delReq)
if err != nil {
return
}
}
}
if group {
for _,req := range AddReq_groups {
delReq := ldap.NewDelRequest(req.DN, nil)
err = l.Del(delReq)
if err != nil {
return
}
}
}
defer log.Debug("Fin clean")
return
}
func test_modify_attributes(l *ldap.Conn, r *rand.Rand, tab_AddReq []ldap.AddRequest, tab_type_name []string) (err error) {
for _, AddReq := range tab_AddReq {
modReq := ldap.NewModifyRequest(AddReq.DN,nil)
for _, type_name := range tab_type_name {
newName := generateName(r)
modReq.Replace(type_name, []string{newName})
att := search_attributes(AddReq.Attributes, type_name)
att.Vals[0] = newName
}
err = l.Modify(modReq)
if err != nil {
return
}
}
return
}
func add_user_in_groups(l *ldap.Conn, r *rand.Rand, users, groups []ldap.AddRequest) (err error) {
for _,group := range groups {
numberUsers := r.Intn(19) + 1 //Always a minimum of 1 user
list_users := []string{}
for i:=0; i < numberUsers; i++ {
list_users = append(list_users, users[i].DN)
}
modifyReq := ldap.NewModifyRequest( group.DN, nil)
modifyReq.Add("member", list_users)
err = l.Modify(modifyReq)
if err != nil {
log.Warn(fmt.Sprintf("Error: ModifyReq failed, func:add_users_in_groups from group:\n %d",group))
return
}
}
return
}
func delete_groups(l *ldap.Conn, groups []ldap.AddRequest) (list map[string][]string ,err error) {
list = make(map[string][]string)
for _, group := range groups {
//Get lists_users
cn := strings.Split(group.DN,",")[0]
search_req := ldap.NewSearchRequest(
"ou=groups,dc=deuxfleurs,dc=fr",
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
fmt.Sprintf("(&(objectclass=groupOfNames)(%s))",cn),
[]string{"member"},
nil,
)
res , err := l.Search(search_req)
if err != nil {
log.Warn(fmt.Sprintf("Error Search: func: delete_groups_and_check_memberOf, from group: \n %d", group))
return list, err
}
if len(res.Entries) != 1 {
err = errors.New(fmt.Sprintf("SearchResult get: %s, SearchResult wanted: 1", len(res.Entries)))
return list, err
}
EntryAtt := res.Entries[0].Attributes
list_users := []string{}
for _, att := range EntryAtt {
list_users = append(list_users ,att.Values[0])
}
//Del group
del := ldap.NewDelRequest( group.DN, nil)
err = l.Del(del)
if err != nil {
return list, err
}
list[group.DN] = list_users
}
return
}
func check_memberOf(l *ldap.Conn, list map[string][]string) (err error) {
//Check the memberOf of all users
for groupeDN,_ := range list{
search_req := ldap.NewSearchRequest(
"ou=users,dc=deuxfleurs,dc=fr",
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases,0 ,0, false,
fmt.Sprintf("(&(objectclass=inetOrgPerson)(memberOf=%s))",groupeDN),
[]string{"cn"},
nil,
)
res, err := l.Search(search_req)
if err != nil {
return err
}
if len(res.Entries) != 0 {
err = errors.New(fmt.Sprintf("L'user '%s' a encore le DN d'un groupe supprimé: %s",res.Entries[0].Attributes[0].Values[0],groupeDN))
return err
}
}
return err
}
func reconnect(l *ldap.Conn) (l_nouv *ldap.Conn, err error){
l.Close()
l_nouv, err = ldap.Dial("tcp", fmt.Sprintf("%s:%d",adresse,port))
if err != nil {
return
}
err = l_nouv.Bind(bindusername, bindpassword)
return
}
func main() {
var ok bool
bindpassword, ok = os.LookupEnv("BOTTIN_DEFAULT_ADMIN_PW")
if !ok {
if len(os.Args) == 2 {
bindpassword = os.Args[1]
} else {
bindpassword = ""
}
}
log.Info(fmt.Sprintf("Password selected: %s",bindpassword))
//log.SetLevel(log.TraceLevel)
//Create a connection with Bottin server
l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", adresse, port))
//l.Debug = true
printError(err)
//Bind with the admin account generated
err = l.Bind(bindusername, bindpassword)
printError(err)
//Create our object Rand, it's important to always have the same values
source := rand.NewSource(666475745)
r := rand.New(source)
log.Info(fmt.Sprintf("The seed of the rand object is %d.\n",r.Seed))
//Create user and groups OrgaUnit
err = createOU(l)
if ldap.IsErrorWithCode(err, uint16(68)) {
log.Warn("Les OrganizationalUnit users et groups sont déjà présents.")
}else {
printError(err)
log.Info("Création des OU de groups et users")
}
//Create random groups
tab_AddRequest_groups, err := createGroup(r, l)
printError(err)
log.Info(fmt.Sprintf("Création des groupes aléatoirement réussi: %d\n", len(tab_AddRequest_groups)))
//Create random users
tab_AddRequest_users, err := createUser(r, l)
printError(err)
log.Info(fmt.Sprintf("Création des users aléatoirement réussi: %d\n", len(tab_AddRequest_users)))
//Search and compare attribute Users. (We keep Attribute object from 'Create random users' and compare with the result of our search)
err = test_attributes(l,tab_AddRequest_users, "inetOrgPerson","users")
printError(err)
log.Info("Tous les attributs users insérés dans Consul ont été vérifiés..\n")
//Search and compare attributes Groups
err = test_attributes(l,tab_AddRequest_groups, "groupOfNames","groups")
printError(err)
log.Info("Tous les attributs groups insérés dans Consul ont été vérifiés.\n")
//Close the connection and open an other. If we don't do this, bottin server send a wrong answer. Comment this part if you want to try this
l,err = reconnect(l)
printError(err)
//Modify attributes users and groups.
//Modify users' attributes and check them
log.Debug(fmt.Sprintf("Les valeurs sont:\n %s", tab_AddRequest_users))
err = test_modify_attributes(l, r, tab_AddRequest_users, []string{"displayname"})
printError(err)
log.Debug("Modifications users faites")
//Check if the attributes are correct:
err = test_attributes(l,tab_AddRequest_users, "inetOrgPerson", "users")
printError(err)
log.Info("Les modifications ont bien été prises en compte")
log.Debug(fmt.Sprintf("Les nouvelles valeurs sont:\n %s", tab_AddRequest_users))
//Modify users' attributes and check them
err = test_modify_attributes(l, r, tab_AddRequest_groups, []string{"description"})
printError(err)
log.Info("Modifications groups faites")
//Check if the attributes are correct:
err = test_attributes(l,tab_AddRequest_groups, "groupOfNames", "groups")
printError(err)
log.Info("Les modifications ont bien été prises en compte")
//Close the connection
l, err = reconnect(l)
printError(err)
//Add users in group, search them, delete several samples and search again to be sur it's good
err = add_user_in_groups(l, r, tab_AddRequest_users, tab_AddRequest_groups)
printError(err)
log.Info("Ajout d'users dans les groupes fait")
//Close the connection
l, err = reconnect(l)
printError(err)
list, err := delete_groups(l, tab_AddRequest_groups)
printError(err)
log.Info("groupe supprimé")
l,err = reconnect(l)
printError(err)
err = check_memberOf(l, list)
printError(err)
log.Info("Le memberOf a été correctement vidé")
//Clean: Delete all users and groups (not OU users and groups)
err = clean(l, tab_AddRequest_users, tab_AddRequest_groups, true, false)
printError(err)
log.Info("Clean succes")
defer os.Exit(0)
return
}

View file

@ -1,40 +0,0 @@
Introduction et Observation premières:
Lors de la réalisation de mon code Go, j'ai trouvé un beug qui provoquait l'arrêt de mon programme car Bottin envoyé une réponse erronée à mon programme.
Dans logs de Bottin je ne voyais aucune erreur, Bottin n'avait pas cessé de fonctionner. Il s'arrêtait à chaque fois sur mes requêtes Del (mes dernières).
Reproduction du beug:
Pour reproduire le beug, il suffit de lancer le programme interrogation.go et de commenter les lignes suivantes :
260 //Close the connection and open an other. If we don't do this, bottin server send a wrong answer. Comment this part if you want to try this
261 l.Close()
262 l, err = ldap.Dial("tcp", fmt.Sprintf("%s:%d",adresse, port))
263 printError(err)
264 err = l.Bind(bindusername, bindpassword)
265 printError(err)
Ainsi on obtient l'erreur suivante:
2021/07/07 01:25:51 Received unexpected message -128, false
Test réalisé pour comprendre la source du problème:
Ma première hypothèses fut que j'envoyais trop de requêtes dans un court laps de temps et ainsi Bottin ou Consul ne pouvait pas suivre.
J'ai placé un sleep de 50 puis de 100 Millisecondes entre chaque requête, j'obtenais toujours la même erreur.
J'ai essayé de mettre un sleep de 10 secondes avant mes requêtes de suppression mais j'obtenais toujours la même chose.
Hack pour résoudre:
La première solution qui a fonctionné était de réduire le nombre de requêtes en n'exécutant pas certains tests random.
La dernière solutions qui est utilisée:
Fermée la connexion puis la réouvrir permet de palier à ce problème
Hypothèses du problème ?:
Existerait-il un Buffer par Bind qui se remplirait ? Et lorsque celui-ci est plein, ne renvoie pas d'erreur juste n'arrive plus à répondre.
Erwan DUFOUR
Deuxfleurs

View file

@ -1,12 +0,0 @@
#!/bin/sh
#export BOTTIN_DEFAULT_ADMIN_PW=$(openssl rand -base64 24)
echo $BOTTIN_DEFAULT_ADMIN_PW
consul agent -dev > /dev/null 2>&1 &
sleep 2
cp test_automatic/config.json.test config.json
./bottin > /dev/null 2>&1 &
sleep 1
./test_automatic/integration
rm config.json
exit 0