diff --git a/central_auth.go b/central_auth.go index 539f058..a409b89 100644 --- a/central_auth.go +++ b/central_auth.go @@ -14,6 +14,7 @@ import ( // centralAuth holds the logic related to handling public keys and auth maps. type centralAuth struct { // schema map[Node]map[argsString]signatureBase32 + authorization *authorization nodePublicKeys *nodePublicKeys nodeNotAckedPublicKeys *nodeNotAckedPublicKeys configuration *Configuration @@ -25,6 +26,7 @@ type centralAuth struct { // newCentralAuth will return a prepared *centralAuth with input values set. func newCentralAuth(configuration *Configuration, errorKernel *errorKernel) *centralAuth { c := centralAuth{ + authorization: newAuthorization(), // schema: make(map[Node]map[argsString]signatureBase32), nodePublicKeys: newNodePublicKeys(configuration), nodeNotAckedPublicKeys: newNodeNotAckedPublicKeys(configuration), diff --git a/central_auth_acl_handling.go b/central_auth_acl_handling.go new file mode 100644 index 0000000..35042d9 --- /dev/null +++ b/central_auth_acl_handling.go @@ -0,0 +1,543 @@ +package steward + +import ( + "crypto/sha256" + "encoding/json" + "fmt" + "log" + "os" + "sort" + "strings" + "sync" + + "github.com/fxamacker/cbor/v2" + "github.com/go-playground/validator/v10" +) + +// // centralAuth +// type centralAuth struct { +// authorization *authorization +// } +// +// // newCentralAuth +// func newCentralAuth() *centralAuth { +// c := centralAuth{ +// authorization: newAuthorization(), +// } +// +// return &c +// } + +// -------------------------------------- + +type authorization struct { + authSchema *authSchema +} + +func newAuthorization() *authorization { + a := authorization{ + authSchema: newAuthSchema(), + } + + return &a +} + +// authSchema holds both the main schema to update by operators, +// and also the indvidual node generated data based on the main schema. +type authSchema struct { + // Holds the editable structures for ACL handling. + schemaMain *schemaMain + // Holds the generated based on the editable structures for ACL handling. + schemaGenerated *schemaGenerated + validator *validator.Validate +} + +func newAuthSchema() *authSchema { + a := authSchema{ + schemaMain: newSchemaMain(), + schemaGenerated: newSchemaGenerated(), + validator: validator.New(), + } + + return &a +} + +type node string +type command string +type nodeGroup string +type commandGroup string + +// schemaMain is the structure that holds the user editable parts for creating ACL's. +type schemaMain struct { + ACLMap map[node]map[node]map[command]struct{} + NodeGroupMap map[nodeGroup]map[node]struct{} + CommandGroupMap map[commandGroup]map[command]struct{} + mu sync.Mutex +} + +func newSchemaMain() *schemaMain { + s := schemaMain{ + ACLMap: make(map[node]map[node]map[command]struct{}), + NodeGroupMap: make(map[nodeGroup]map[node]struct{}), + CommandGroupMap: make(map[commandGroup]map[command]struct{}), + } + return &s +} + +// schemaGenerated is the structure that holds all the generated ACL's +// to be sent to nodes. +// The ACL's here are generated from the schemaMain.ACLMap. +type schemaGenerated struct { + ACLsToConvert map[node]map[node]map[command]struct{} + GeneratedACLsMap map[node]HostACLsSerializedWithHash + mu sync.Mutex +} + +func newSchemaGenerated() *schemaGenerated { + s := schemaGenerated{ + ACLsToConvert: map[node]map[node]map[command]struct{}{}, + GeneratedACLsMap: make(map[node]HostACLsSerializedWithHash), + } + return &s +} + +// HostACLsSerializedWithHash holds the serialized representation node specific ACL's in the authSchema. +// There is also a sha256 hash of the data. +type HostACLsSerializedWithHash struct { + // data is all the ACL's for a specific node serialized. + Data []byte + // hash is the sha256 hash of the ACL's. + // With maps the order are not guaranteed, so A sorted appearance + // of the ACL map for a host node is used when creating the hash, + // so the hash stays the same unless the ACL is changed. + Hash [32]byte +} + +// commandAsSlice will convert the given argument into a slice representation. +// If the argument is a group, then all the members of that group will be expanded into +// the slice. +// If the argument is not a group kind of value, then only a slice with that single +// value is returned. +func (a *authSchema) nodeAsSlice(n node) []node { + nodes := []node{} + + // Check if we are given a nodeGroup variable, and if we are, get all the + // nodes for that group. + if strings.HasPrefix(string(n), "grp_nodes_") { + for nd := range a.schemaMain.NodeGroupMap[nodeGroup(n)] { + nodes = append(nodes, nd) + } + } else { + // No group found meaning a single node was given as an argument. + nodes = []node{n} + } + + return nodes +} + +// commandAsSlice will convert the given argument into a slice representation. +// If the argument is a group, then all the members of that group will be expanded into +// the slice. +// If the argument is not a group kind of value, then only a slice with that single +// value is returned. +func (a *authSchema) commandAsSlice(c command) []command { + commands := []command{} + + // Check if we are given a nodeGroup variable, and if we are, get all the + // nodes for that group. + if strings.HasPrefix(string(c), "grp_commands_") { + for cmd := range a.schemaMain.CommandGroupMap[commandGroup(c)] { + commands = append(commands, cmd) + } + } else { + // No group found meaning a single node was given as an argument, so we + // just put the single node given as the only value in the slice. + commands = []command{c} + } + + return commands +} + +// aclAdd will add a command for a fromNode. +// If the node or the fromNode do not exist they will be created. +// The json encoded schema for a node and the hash of those data +// will also be generated. +func (a *authSchema) aclAdd(host node, source node, cmd command) { + a.schemaMain.mu.Lock() + defer a.schemaMain.mu.Unlock() + + // Check if node exists in map. + if _, ok := a.schemaMain.ACLMap[host]; !ok { + // log.Printf("info: did not find node=%v in map, creating map[fromnode]map[command]struct{}\n", n) + a.schemaMain.ACLMap[host] = make(map[node]map[command]struct{}) + } + + // Check if also source node exists in map + if _, ok := a.schemaMain.ACLMap[host][source]; !ok { + // log.Printf("info: did not find node=%v in map, creating map[fromnode]map[command]struct{}\n", fn) + a.schemaMain.ACLMap[host][source] = make(map[command]struct{}) + } + + a.schemaMain.ACLMap[host][source][cmd] = struct{}{} + // err := a.generateJSONForHostOrGroup(n) + err := a.generateACLsForAllNodes() + if err != nil { + er := fmt.Errorf("error: addCommandForFromNode: %v", err) + log.Printf("%v\n", er) + } + + // fmt.Printf(" * DEBUG: aclNodeFromnodeCommandAdd: a.schemaMain.ACLMap=%v\n", a.schemaMain.ACLMap) + +} + +// aclDeleteCommand will delete the specified command from the fromnode. +func (a *authSchema) aclDeleteCommand(host node, source node, cmd command) error { + a.schemaMain.mu.Lock() + defer a.schemaMain.mu.Unlock() + + // Check if node exists in map. + if _, ok := a.schemaMain.ACLMap[host]; !ok { + return fmt.Errorf("authSchema: no such node=%v to delete on in schema exists", host) + } + + if _, ok := a.schemaMain.ACLMap[host][source]; !ok { + return fmt.Errorf("authSchema: no such fromnode=%v to delete on in schema for node=%v exists", source, host) + } + + if _, ok := a.schemaMain.ACLMap[host][source][cmd]; !ok { + return fmt.Errorf("authSchema: no such command=%v from fromnode=%v to delete on in schema for node=%v exists", cmd, source, host) + } + + delete(a.schemaMain.ACLMap[host][source], cmd) + + err := a.generateACLsForAllNodes() + if err != nil { + er := fmt.Errorf("error: aclNodeFromNodeCommandDelete: %v", err) + log.Printf("%v\n", er) + } + + return nil +} + +// aclDeleteSource will delete specified source node and all commands specified for it. +func (a *authSchema) aclDeleteSource(host node, source node) error { + a.schemaMain.mu.Lock() + defer a.schemaMain.mu.Unlock() + + // Check if node exists in map. + if _, ok := a.schemaMain.ACLMap[host]; !ok { + return fmt.Errorf("authSchema: no such node=%v to delete on in schema exists", host) + } + + if _, ok := a.schemaMain.ACLMap[host][source]; !ok { + return fmt.Errorf("authSchema: no such fromnode=%v to delete on in schema for node=%v exists", source, host) + } + + delete(a.schemaMain.ACLMap[host], source) + + err := a.generateACLsForAllNodes() + if err != nil { + er := fmt.Errorf("error: aclNodeFromnodeDelete: %v", err) + log.Printf("%v\n", er) + } + + return nil +} + +// generateACLsForAllNodes will generate a json encoded representation of the node specific +// map values of authSchema, along with a hash of the data. +// +// Will range over all the host elements defined in the ACL, create a new authParser for each one, +// and run a small state machine on each element to create the final ACL result to be used at host +// nodes. +// The result will be written to the schemaGenerated.ACLsToConvert map. +func (a *authSchema) generateACLsForAllNodes() error { + a.schemaGenerated.mu.Lock() + defer a.schemaGenerated.mu.Unlock() + + a.schemaGenerated.ACLsToConvert = make(map[node]map[node]map[command]struct{}) + + // Rangle all ACL's. Both for single hosts, and group of hosts. + // ACL's that are for a group of hosts will be generated split + // out in it's indivial host name, and that current ACL will + // be added to the individual host in the ACLsToConvert map to + // built a complete picture of what the ACL's looks like for each + // individual hosts. + for n := range a.schemaMain.ACLMap { + //a.schemaGenerated.ACLsToConvert = make(map[node]map[node]map[command]struct{}) + ap := newAuthParser(n, a) + ap.parse() + } + + // ACLsToConvert got the complete picture of what ACL's that + // are defined for each individual host node. + // Range this map, and generate a JSON representation of all + // the ACL's each host. + func() { + for n, m := range a.schemaGenerated.ACLsToConvert { + + // cbor marshal the data of the ACL map to store for the host node. + cb, err := cbor.Marshal(m) + if err != nil { + er := fmt.Errorf("error: failed to generate json for host in schemaGenerated: %v", err) + log.Printf("%v\n", er) + os.Exit(1) + } + + // Create the hash for the data for the host node. + hash := func() [32]byte { + sns := a.nodeMapToSlice(n) + + b, err := cbor.Marshal(sns) + if err != nil { + err := fmt.Errorf("error: authSchema, json for hash: %v", err) + log.Printf("%v\n", err) + return [32]byte{} + } + + hash := sha256.Sum256(b) + return hash + }() + + // Store both the cbor marshaled data and the hash in a structure. + hostSerialized := HostACLsSerializedWithHash{ + Data: cb, + Hash: hash, + } + + // and then store the cbor encoded data and the hash in the generated map. + a.schemaGenerated.GeneratedACLsMap[n] = hostSerialized + + } + }() + + return nil +} + +// sourceNode is used to convert the ACL map structure of a host into a slice, +// and we then use the slice representation of the ACL to create the hash for +// a specific host node. +type sourceNode struct { + HostNode node + SourceCommands []sourceNodeCommands +} + +// sourceNodeCommand is used to convert the ACL map structure of a host into a slice, +// and we then use the slice representation of the ACL to create the hash for +// a specific host node. +type sourceNodeCommands struct { + Source node + Commands []command +} + +// nodeMapToSlice will return a sourceNode structure, with the map sourceNode part +// of the map converted into a slice. Both the from node, and the commands +// defined for each sourceNode are sorted. +// This function is used when creating the hash of the nodeMap since we can not +// guarantee the order of a hash map, but we can with a slice. +func (a *authSchema) nodeMapToSlice(host node) sourceNode { + srcNodes := sourceNode{ + HostNode: host, + } + + for sn, commandMap := range a.schemaGenerated.ACLsToConvert[host] { + srcC := sourceNodeCommands{ + Source: sn, + } + + for cmd := range commandMap { + srcC.Commands = append(srcC.Commands, cmd) + } + + // Sort all the commands. + sort.SliceStable(srcC.Commands, func(i, j int) bool { + return srcC.Commands[i] < srcC.Commands[j] + }) + + srcNodes.SourceCommands = append(srcNodes.SourceCommands, srcC) + } + + // Sort all the source nodes. + sort.SliceStable(srcNodes.SourceCommands, func(i, j int) bool { + return srcNodes.SourceCommands[i].Source < srcNodes.SourceCommands[j].Source + }) + + // fmt.Printf(" * nodeMapToSlice: fromNodes: %#v\n", fns) + + return srcNodes +} + +// groupNodesAddNode adds a node to a group. If the group does +// not exist it will be created. +func (a *authSchema) groupNodesAddNode(ng nodeGroup, n node) { + err := a.validator.Var(ng, "startswith=grp_nodes_") + if err != nil { + log.Printf("error: group name do not start with grp_nodes_: %v\n", err) + return + } + + a.schemaMain.mu.Lock() + defer a.schemaMain.mu.Unlock() + if _, ok := a.schemaMain.NodeGroupMap[ng]; !ok { + a.schemaMain.NodeGroupMap[ng] = make(map[node]struct{}) + } + + a.schemaMain.NodeGroupMap[ng][n] = struct{}{} + + // fmt.Printf(" * groupNodesAddNode: After adding to group node looks like: %+v\n", a.schemaMain.NodeGroupMap) + + err = a.generateACLsForAllNodes() + if err != nil { + er := fmt.Errorf("error: groupNodesAddNode: %v", err) + log.Printf("%v\n", er) + } + +} + +// groupNodesDeleteNode deletes a node from a group in the map. +func (a *authSchema) groupNodesDeleteNode(ng nodeGroup, n node) { + a.schemaMain.mu.Lock() + defer a.schemaMain.mu.Unlock() + if _, ok := a.schemaMain.NodeGroupMap[ng][n]; !ok { + log.Printf("info: no such node with name=%v found in group=%v\n", ng, n) + return + } + + delete(a.schemaMain.NodeGroupMap[ng], n) + + //fmt.Printf(" * After deleting nodeGroup map looks like: %+v\n", a.schemaMain.NodeGroupMap) + + err := a.generateACLsForAllNodes() + if err != nil { + er := fmt.Errorf("error: groupNodesDeleteNode: %v", err) + log.Printf("%v\n", er) + } + +} + +// groupNodesDeleteGroup deletes a nodeGroup from map. +func (a *authSchema) groupNodesDeleteGroup(ng nodeGroup) { + a.schemaMain.mu.Lock() + defer a.schemaMain.mu.Unlock() + if _, ok := a.schemaMain.NodeGroupMap[ng]; !ok { + log.Printf("info: no such group found: %v\n", ng) + return + } + + delete(a.schemaMain.NodeGroupMap, ng) + + //fmt.Printf(" * After deleting nodeGroup map looks like: %+v\n", a.schemaMain.NodeGroupMap) + + err := a.generateACLsForAllNodes() + if err != nil { + er := fmt.Errorf("error: groupNodesDeleteGroup: %v", err) + log.Printf("%v\n", er) + } + +} + +// ----- + +// groupCommandsAddCommand adds a command to a group. If the group does +// not exist it will be created. +func (a *authSchema) groupCommandsAddCommand(cg commandGroup, c command) { + err := a.validator.Var(cg, "startswith=grp_commands_") + if err != nil { + log.Printf("error: group name do not start with grp_commands_ : %v\n", err) + return + } + + a.schemaMain.mu.Lock() + defer a.schemaMain.mu.Unlock() + if _, ok := a.schemaMain.CommandGroupMap[cg]; !ok { + a.schemaMain.CommandGroupMap[cg] = make(map[command]struct{}) + } + + a.schemaMain.CommandGroupMap[cg][c] = struct{}{} + + //fmt.Printf(" * groupCommandsAddCommand: After adding command=%v to command group=%v map looks like: %+v\n", c, cg, a.schemaMain.CommandGroupMap) + + err = a.generateACLsForAllNodes() + if err != nil { + er := fmt.Errorf("error: groupCommandsAddCommand: %v", err) + log.Printf("%v\n", er) + } + +} + +// groupCommandsDeleteCommand deletes a command from a group in the map. +func (a *authSchema) groupCommandsDeleteCommand(cg commandGroup, c command) { + a.schemaMain.mu.Lock() + defer a.schemaMain.mu.Unlock() + if _, ok := a.schemaMain.CommandGroupMap[cg][c]; !ok { + log.Printf("info: no such command with name=%v found in group=%v\n", c, cg) + return + } + + delete(a.schemaMain.CommandGroupMap[cg], c) + + //fmt.Printf(" * After deleting command=%v from group=%v map looks like: %+v\n", c, cg, a.schemaMain.CommandGroupMap) + + err := a.generateACLsForAllNodes() + if err != nil { + er := fmt.Errorf("error: groupCommandsDeleteCommand: %v", err) + log.Printf("%v\n", er) + } + +} + +// groupCommandDeleteGroup deletes a commandGroup map. +func (a *authSchema) groupCommandDeleteGroup(cg commandGroup) { + a.schemaMain.mu.Lock() + defer a.schemaMain.mu.Unlock() + if _, ok := a.schemaMain.CommandGroupMap[cg]; !ok { + log.Printf("info: no such group found: %v\n", cg) + return + } + + delete(a.schemaMain.CommandGroupMap, cg) + + //fmt.Printf(" * After deleting commandGroup=%v map looks like: %+v\n", cg, a.schemaMain.CommandGroupMap) + + err := a.generateACLsForAllNodes() + if err != nil { + er := fmt.Errorf("error: groupCommandDeleteGroup: %v", err) + log.Printf("%v\n", er) + } + +} + +// exportACLs will export the current content of the main ACLMap in JSON format. +func (a *authSchema) exportACLs() ([]byte, error) { + + a.schemaMain.mu.Lock() + defer a.schemaMain.mu.Unlock() + + js, err := json.Marshal(a.schemaMain.ACLMap) + if err != nil { + return nil, fmt.Errorf("error: failed to marshal schemaMain.ACLMap: %v", err) + + } + + return js, nil + +} + +// importACLs will import and replace all current ACL's with the ACL's provided as input. +func (a *authSchema) importACLs(js []byte) error { + + a.schemaMain.mu.Lock() + defer a.schemaMain.mu.Unlock() + + m := make(map[node]map[node]map[command]struct{}) + + err := json.Unmarshal(js, &m) + if err != nil { + return fmt.Errorf("error: failed to unmarshal into ACLMap: %v", err) + } + + a.schemaMain.ACLMap = m + + return nil + +} diff --git a/central_auth_parser.go b/central_auth_parser.go new file mode 100644 index 0000000..68250cb --- /dev/null +++ b/central_auth_parser.go @@ -0,0 +1,118 @@ +package steward + +import ( + "strings" +) + +type authParser struct { + currentHost node + authSchema *authSchema + //ACLsToConvert map[node]map[node]map[command]struct{} +} + +// newAuthParser returns a new authParser, with the current host node set. +func newAuthParser(n node, authSchema *authSchema) *authParser { + a := authParser{ + currentHost: n, + authSchema: authSchema, + //ACLsToConvert: make(map[node]map[node]map[command]struct{}), + } + return &a +} + +type parseFn func() parseFn + +// parse will parse one host or one host group. +func (a *authParser) parse() { + fn := a.hostGroupOrSingle() + for { + fn = fn() + if fn == nil { + break + } + } + +} + +// hostGroupOrSingle checks if host grp or single node. +func (a *authParser) hostGroupOrSingle() parseFn { + switch { + case strings.HasPrefix(string(a.currentHost), "grp_nodes_"): + // Is group + return a.hostIsGroup + default: + // Is single node + return a.hostIsNotGroup + } +} + +// hostIsGroup +func (a *authParser) hostIsGroup() parseFn { + // fmt.Printf("%v is a grp type\n", a.currentHost) + + hosts := a.authSchema.nodeAsSlice(a.currentHost) + + for source, cmdMap := range a.authSchema.schemaMain.ACLMap[a.currentHost] { + + for cmd, emptyStruct := range cmdMap { + cmdSlice := a.authSchema.commandAsSlice(cmd) + + // Expand eventual groups, so we use real fromNode nodenames in ACL for nodes. + sourceNodes := a.authSchema.nodeAsSlice(source) + for _, sourceNode := range sourceNodes { + for _, host := range hosts { + + for _, cm := range cmdSlice { + if a.authSchema.schemaGenerated.ACLsToConvert[host] == nil { + a.authSchema.schemaGenerated.ACLsToConvert[host] = make(map[node]map[command]struct{}) + } + if a.authSchema.schemaGenerated.ACLsToConvert[host][sourceNode] == nil { + a.authSchema.schemaGenerated.ACLsToConvert[host][sourceNode] = make(map[command]struct{}) + } + + a.authSchema.schemaGenerated.ACLsToConvert[host][sourceNode][cm] = emptyStruct + } + } + } + } + } + + // fmt.Printf(" * ACLsToConvert=%+v\n", a.authSchema.schemaGenerated.ACLsToConvert) + // Done with host. Return nil will make the main loop take the next host in the main for loop. + return nil +} + +// hostIsNotGroup +func (a *authParser) hostIsNotGroup() parseFn { + // fmt.Printf("%v is a single node type\n", a.currentHost) + + host := a.currentHost + + for source, cmdMap := range a.authSchema.schemaMain.ACLMap[a.currentHost] { + + for cmd, emptyStruct := range cmdMap { + cmdSlice := a.authSchema.commandAsSlice(cmd) + + // Expand eventual groups, so we use real fromNode nodenames in ACL for nodes. + sourceNodes := a.authSchema.nodeAsSlice(source) + for _, sourceNode := range sourceNodes { + + for _, cm := range cmdSlice { + if a.authSchema.schemaGenerated.ACLsToConvert[host] == nil { + a.authSchema.schemaGenerated.ACLsToConvert[host] = make(map[node]map[command]struct{}) + } + if a.authSchema.schemaGenerated.ACLsToConvert[host][sourceNode] == nil { + a.authSchema.schemaGenerated.ACLsToConvert[host][sourceNode] = make(map[command]struct{}) + } + + a.authSchema.schemaGenerated.ACLsToConvert[host][sourceNode][cm] = emptyStruct + } + } + } + } + + // fmt.Printf(" * ACLsToConvert contains: %+v\n", a.authSchema.schemaGenerated.ACLsToConvert) + + // Done with host. Return nil will make the main loop take the next host in the main for loop. + return nil +} diff --git a/central_auth_test.go b/central_auth_test.go new file mode 100644 index 0000000..a222582 --- /dev/null +++ b/central_auth_test.go @@ -0,0 +1,514 @@ +package steward + +import ( + "bytes" + "fmt" + "io" + "log" + "sync" + "testing" + + "github.com/fxamacker/cbor/v2" +) + +func TestACLSingleNode(t *testing.T) { + if !*logging { + log.SetOutput(io.Discard) + } + + a := newAuthSchema() + a.aclAdd("ship101", "admin", "HORSE") + a.aclAdd("ship101", "admin", "PIG") + + // --- TESTS --- + + mapOfFromNodeCommands := make(map[node]map[command]struct{}) + err := cbor.Unmarshal(a.schemaGenerated.GeneratedACLsMap["ship101"].Data, &mapOfFromNodeCommands) + if err != nil { + t.Fatal(err) + } + + if _, ok := mapOfFromNodeCommands["admin"]["HORSE"]; !ok { + t.Fatal(" \U0001F631 [FAILED]: missing map entry") + } + + if _, ok := mapOfFromNodeCommands["admin"]["PIG"]; !ok { + t.Fatal(" \U0001F631 [FAILED]: missing map entry") + } +} + +func TestACLWithGroups(t *testing.T) { + if !*logging { + log.SetOutput(io.Discard) + } + + a := newAuthSchema() + + const ( + grp_nodes_operators = "grp_nodes_operators" + grp_nodes_ships = "grp_nodes_ships" + grp_commands_commandset1 = "grp_commands_commandset1" + ) + + a.groupNodesAddNode(grp_nodes_operators, "operator1") + a.groupNodesAddNode(grp_nodes_operators, "operator2") + + a.groupNodesAddNode(grp_nodes_ships, "ship100") + a.groupNodesAddNode(grp_nodes_ships, "ship101") + + a.groupCommandsAddCommand(grp_commands_commandset1, "dmesg") + a.groupCommandsAddCommand(grp_commands_commandset1, "date") + + a.aclAdd(grp_nodes_ships, "admin", "useradd -m kongen") + a.aclAdd("ship101", "admin", "HORSE") + + a.aclAdd(grp_nodes_ships, grp_nodes_operators, grp_commands_commandset1) + + mapOfFromNodeCommands := make(map[node]map[command]struct{}) + err := cbor.Unmarshal(a.schemaGenerated.GeneratedACLsMap["ship101"].Data, &mapOfFromNodeCommands) + if err != nil { + t.Fatal(err) + } + + if _, ok := mapOfFromNodeCommands["admin"]["useradd -m kongen"]; !ok { + t.Fatal(" \U0001F631 [FAILED]: missing map entry") + } + + if _, ok := mapOfFromNodeCommands["operator1"]["dmesg"]; !ok { + t.Fatal(" \U0001F631 [FAILED]: missing map entry") + } + + if _, ok := mapOfFromNodeCommands["operator1"]["date"]; !ok { + t.Fatal(" \U0001F631 [FAILED]: missing map entry") + } + + if _, ok := mapOfFromNodeCommands["operator2"]["dmesg"]; !ok { + t.Fatal(" \U0001F631 [FAILED]: missing map entry") + } + + if _, ok := mapOfFromNodeCommands["operator2"]["date"]; !ok { + t.Fatal(" \U0001F631 [FAILED]: missing map entry") + } + + if _, ok := mapOfFromNodeCommands["admin"]["HORSE"]; !ok { + t.Fatal(" \U0001F631 [FAILED]: missing map entry") + } + +} + +func TestACLNodesGroupDeleteNode(t *testing.T) { + if !*logging { + log.SetOutput(io.Discard) + } + + a := newAuthSchema() + + const ( + grp_nodes_operators = "grp_nodes_operators" + grp_nodes_ships = "grp_nodes_ships" + grp_commands_commandset1 = "grp_commands_commandset1" + ) + + a.groupNodesAddNode(grp_nodes_operators, "operator1") + a.groupNodesAddNode(grp_nodes_operators, "operator2") + + a.groupNodesAddNode(grp_nodes_ships, "ship100") + a.groupNodesAddNode(grp_nodes_ships, "ship101") + + a.groupCommandsAddCommand(grp_commands_commandset1, "dmesg") + a.groupCommandsAddCommand(grp_commands_commandset1, "date") + + a.aclAdd(grp_nodes_ships, "admin", "useradd -m kongen") + a.aclAdd("ship101", "admin", "HORSE") + + a.aclAdd(grp_nodes_ships, grp_nodes_operators, grp_commands_commandset1) + + a.groupNodesDeleteNode(grp_nodes_ships, "ship101") + + // Check that we still got the data for ship100. + { + mapOfFromNodeCommands := make(map[node]map[command]struct{}) + err := cbor.Unmarshal(a.schemaGenerated.GeneratedACLsMap["ship100"].Data, &mapOfFromNodeCommands) + if err != nil { + t.Fatal(err) + } + + if _, ok := mapOfFromNodeCommands["admin"]["useradd -m kongen"]; !ok { + t.Fatal(" \U0001F631 [FAILED]: missing map entry") + } + } + + // Check that we don't have any data for ship101. + { + mapOfFromNodeCommands := make(map[node]map[command]struct{}) + err := cbor.Unmarshal(a.schemaGenerated.GeneratedACLsMap["ship101"].Data, &mapOfFromNodeCommands) + if err != nil { + t.Fatal(err) + } + + if _, ok := mapOfFromNodeCommands["admin"]["useradd -m kongen"]; ok { + t.Fatal(" \U0001F631 [FAILED]: missing map entry") + } + } + +} + +func TestGroupNodesDeleteGroup(t *testing.T) { + if !*logging { + log.SetOutput(io.Discard) + } + + a := newAuthSchema() + + const ( + grp_nodes_operators = "grp_nodes_operators" + grp_nodes_ships = "grp_nodes_ships" + grp_commands_commandset1 = "grp_commands_commandset1" + ) + + a.groupNodesAddNode(grp_nodes_operators, "operator1") + a.groupNodesAddNode(grp_nodes_operators, "operator2") + + a.groupNodesAddNode(grp_nodes_ships, "ship100") + a.groupNodesAddNode(grp_nodes_ships, "ship101") + + a.groupCommandsAddCommand(grp_commands_commandset1, "dmesg") + a.groupCommandsAddCommand(grp_commands_commandset1, "date") + + a.aclAdd(grp_nodes_ships, "admin", "useradd -m kongen") + a.aclAdd("ship101", "admin", "HORSE") + + a.aclAdd(grp_nodes_ships, grp_nodes_operators, grp_commands_commandset1) + + a.groupNodesDeleteGroup(grp_nodes_operators) + + // Check that we still got the data for other ACL's. + { + mapOfFromNodeCommands := make(map[node]map[command]struct{}) + err := cbor.Unmarshal(a.schemaGenerated.GeneratedACLsMap["ship101"].Data, &mapOfFromNodeCommands) + if err != nil { + t.Fatal(err) + } + + if _, ok := mapOfFromNodeCommands["admin"]["HORSE"]; !ok { + t.Fatal(" \U0001F631 [FAILED]: missing map entry") + } + } + + // Check that we don't have any data for grp_nodes_operators + { + mapOfFromNodeCommands := make(map[node]map[command]struct{}) + err := cbor.Unmarshal(a.schemaGenerated.GeneratedACLsMap["ship101"].Data, &mapOfFromNodeCommands) + if err != nil { + t.Fatal(err) + } + + if _, ok := mapOfFromNodeCommands["admin"]["dmesg"]; ok { + t.Fatal(" \U0001F631 [FAILED]: foud map entry") + } + } + +} + +func TestGroupCommandDeleteGroup(t *testing.T) { + if !*logging { + log.SetOutput(io.Discard) + } + + a := newAuthSchema() + + const ( + grp_nodes_operators = "grp_nodes_operators" + grp_nodes_ships = "grp_nodes_ships" + grp_commands_commandset1 = "grp_commands_commandset1" + ) + + a.groupNodesAddNode(grp_nodes_operators, "operator1") + a.groupNodesAddNode(grp_nodes_operators, "operator2") + + a.groupNodesAddNode(grp_nodes_ships, "ship100") + a.groupNodesAddNode(grp_nodes_ships, "ship101") + + a.groupCommandsAddCommand(grp_commands_commandset1, "dmesg") + a.groupCommandsAddCommand(grp_commands_commandset1, "date") + + a.aclAdd(grp_nodes_ships, "admin", "useradd -m kongen") + a.aclAdd("ship101", "admin", "HORSE") + + a.aclAdd(grp_nodes_ships, grp_nodes_operators, grp_commands_commandset1) + + a.groupCommandDeleteGroup(grp_commands_commandset1) + + // Check that we still got the data for other ACL's. + { + mapOfFromNodeCommands := make(map[node]map[command]struct{}) + err := cbor.Unmarshal(a.schemaGenerated.GeneratedACLsMap["ship101"].Data, &mapOfFromNodeCommands) + if err != nil { + t.Fatal(err) + } + + if _, ok := mapOfFromNodeCommands["admin"]["HORSE"]; !ok { + t.Fatal(" \U0001F631 [FAILED]: missing map entry") + } + } + + // Check that we don't have any data for grp_nodes_operators + { + mapOfFromNodeCommands := make(map[node]map[command]struct{}) + err := cbor.Unmarshal(a.schemaGenerated.GeneratedACLsMap["ship101"].Data, &mapOfFromNodeCommands) + if err != nil { + t.Fatal(err) + } + + if _, ok := mapOfFromNodeCommands["admin"]["dmesg"]; ok { + t.Fatal(" \U0001F631 [FAILED]: foud map entry") + } + } + +} + +func TestACLGenerated(t *testing.T) { + if !*logging { + log.SetOutput(io.Discard) + } + + a := newAuthSchema() + + a.aclAdd("ship101", "admin", "HORSE") + + a.groupNodesAddNode("grp_nodes_ships", "ship101") + a.aclAdd("grp_nodes_ships", "admin", "HEN") + + a.groupCommandsAddCommand("grp_commands_test", "echo") + a.groupCommandsAddCommand("grp_commands_test", "dmesg") + a.aclAdd("grp_nodes_ships", "admin", "grp_commands_test") + + a.groupCommandsDeleteCommand("grp_commands_test", "echo") + + // --- TESTS --- + + mapOfFromNodeCommands := make(map[node]map[command]struct{}) + err := cbor.Unmarshal(a.schemaGenerated.GeneratedACLsMap["ship101"].Data, &mapOfFromNodeCommands) + if err != nil { + t.Fatal(err) + } + + //if _, ok := mapOfFromNodeCommands["admin"]["PIG"]; !ok { + // t.Fatalf(" \U0001F631 [FAILED]: missing map entry: PIG: Content of Map: %v", mapOfFromNodeCommands) + //} + + if _, ok := mapOfFromNodeCommands["admin"]["HORSE"]; !ok { + t.Fatalf(" \U0001F631 [FAILED]: missing map entry: HORSE: Content of Map: %v", mapOfFromNodeCommands) + } + + if _, ok := mapOfFromNodeCommands["admin"]["HEN"]; !ok { + t.Fatalf(" \U0001F631 [FAILED]: missing map entry: HEN: Content of Map: %v", mapOfFromNodeCommands) + } + + if _, ok := mapOfFromNodeCommands["admin"]["echo"]; ok { + t.Fatalf(" \U0001F631 [FAILED]: should not contain map entry: echo: Content of Map: %v", mapOfFromNodeCommands) + } + + if _, ok := mapOfFromNodeCommands["admin"]["dmesg"]; !ok { + t.Fatalf(" \U0001F631 [FAILED]: missing map entry: echo: Content of Map: %v", mapOfFromNodeCommands) + + } + +} + +func TestACLSchemaMainACLMap(t *testing.T) { + if !*logging { + log.SetOutput(io.Discard) + } + + a := newAuthSchema() + + //a.aclNodeFromnodeCommandAdd("ship101", "admin", "PIG") + // fmt.Printf("---------------ADDING COMMAND-------------\n") + a.aclAdd("ship0", "admin", "systemctl") + a.aclAdd("ship1", "admin", "tcpdump") + + if _, ok := a.schemaMain.ACLMap["ship0"]["admin"]["systemctl"]; !ok { + t.Fatalf(" \U0001F631 [FAILED]: missing map entry: ship0, admin, systemctl") + } + if _, ok := a.schemaMain.ACLMap["ship1"]["admin"]["tcpdump"]; !ok { + t.Fatalf(" \U0001F631 [FAILED]: missing map entry: ship1, admin, tcpdump") + } + + // fmt.Printf("---------------ADDING COMMAND-------------\n") + a.groupNodesAddNode("grp_nodes_ships", "ship1") + a.groupNodesAddNode("grp_nodes_ships", "ship2") + a.aclAdd("grp_nodes_ships", "admin", "dmesg") + + if _, ok := a.schemaMain.ACLMap["grp_nodes_ships"]["admin"]["dmesg"]; !ok { + t.Fatalf(" \U0001F631 [FAILED]: missing map entry: ship1, admin, tcpdump") + } + + // fmt.Printf("---------------ADDING COMMAND-------------\n") + a.aclAdd("ship2", "admin", "echo") + + if _, ok := a.schemaMain.ACLMap["ship2"]["admin"]["echo"]; !ok { + t.Fatalf(" \U0001F631 [FAILED]: missing map entry: ship1, admin, tcpdump") + } + + // fmt.Printf("---------------DELETING COMMAND grp_nodes_ships, admin, dmesg-------------\n") + a.aclDeleteCommand("grp_nodes_ships", "admin", "dmesg") + + if _, ok := a.schemaMain.ACLMap["grp_nodes_ships"]["admin"]["dmesg"]; ok { + t.Fatalf(" \U0001F631 [FAILED]: found map entry: grp_nodes_ships, admin, dmesg") + } + // Check that the remaining are still ok. + if _, ok := a.schemaMain.ACLMap["ship0"]["admin"]["systemctl"]; !ok { + t.Fatalf(" \U0001F631 [FAILED]: missing map entry: ship0, admin, systemctl") + } + if _, ok := a.schemaMain.ACLMap["ship1"]["admin"]["tcpdump"]; !ok { + t.Fatalf(" \U0001F631 [FAILED]: missing map entry: ship1, admin, tcpdump") + } + if _, ok := a.schemaMain.ACLMap["ship2"]["admin"]["echo"]; !ok { + t.Fatalf(" \U0001F631 [FAILED]: missing map entry: ship1, admin, tcpdump") + } + + // fmt.Printf("---------------DELETING COMMAND ship0, admin, systemctl-------------\n") + a.aclDeleteCommand("ship0", "admin", "systemctl") + + if _, ok := a.schemaMain.ACLMap["ship0"]["admin"]["systemctl"]; ok { + t.Fatalf(" \U0001F631 [FAILED]: missing map entry: ship0, admin, systemctl") + } + // Check that the remaining are ok. + if _, ok := a.schemaMain.ACLMap["ship1"]["admin"]["tcpdump"]; !ok { + t.Fatalf(" \U0001F631 [FAILED]: missing map entry: ship1, admin, tcpdump") + } + if _, ok := a.schemaMain.ACLMap["ship2"]["admin"]["echo"]; !ok { + t.Fatalf(" \U0001F631 [FAILED]: missing map entry: ship1, admin, tcpdump") + } + + // fmt.Printf("---------------DELETING SOURCE ship1, admin-------------\n") + a.aclDeleteSource("ship1", "admin") + + if _, ok := a.schemaMain.ACLMap["ship1"]["admin"]; ok { + t.Fatalf(" \U0001F631 [FAILED]: missing map entry: ship1, admin, tcpdump") + } + // Check that the remaining are ok. + if _, ok := a.schemaMain.ACLMap["ship2"]["admin"]["echo"]; !ok { + t.Fatalf(" \U0001F631 [FAILED]: missing map entry: ship1, admin, tcpdump") + } + // --- TESTS --- +} + +func TestACLHash(t *testing.T) { + if !*logging { + log.SetOutput(io.Discard) + } + + a := newAuthSchema() + + a.aclAdd("ship101", "admin", "HORSE") + + a.groupNodesAddNode("grp_nodes_ships", "ship101") + a.aclAdd("grp_nodes_ships", "admin", "HEN") + + hash := [32]uint8{0xa4, 0x99, 0xbd, 0xa3, 0x18, 0x26, 0x52, 0xc2, 0x92, 0x60, 0x23, 0x19, 0x3c, 0xa, 0x7, 0xa9, 0xb7, 0x77, 0x4f, 0x11, 0x34, 0xd5, 0x2d, 0xd1, 0x8d, 0xab, 0x6c, 0x4b, 0x2, 0xfa, 0x5c, 0x7a} + value := a.schemaGenerated.GeneratedACLsMap["ship101"].Hash + // fmt.Printf("%#v\n", a.schemaGenerated.GeneratedACLsMap["ship101"].Hash) + + if bytes.Equal(hash[:], value[:]) == false { + t.Fatalf(" \U0001F631 [FAILED]: hash mismatch") + } +} + +func TestACLConcurrent(t *testing.T) { + a := newAuthSchema() + + // -----------General testing and creation of some data---------------- + + // Start concurrent updating of the schema. + var wg sync.WaitGroup + for i := 0; i < 4000; i++ { + wg.Add(1) + go func() { + defer wg.Done() + a.aclAdd("ship1", "operator2", "rm -rf") + a.aclAdd("ship1", "operator1", "ls -lt") + a.aclAdd("ship1", "operator1", "ls -lt") + a.aclAdd("ship1", "operator2", "ls -l") + a.aclAdd("ship3", "operator3", "ls -lt") + a.aclAdd("ship3", "operator3", "vi /etc/hostname") + a.aclDeleteCommand("ship3", "operator2", "ls -lt") + a.aclDeleteSource("ship3", "operator3") + }() + + wg.Add(1) + go func() { + defer wg.Done() + // fmt.Println("----schemaMain------") + a.schemaMain.mu.Lock() + for _, v := range a.schemaMain.ACLMap { + _ = fmt.Sprintf("%+v\n", v) + } + a.schemaMain.mu.Unlock() + + // fmt.Println("----schemaGenerated------") + a.schemaGenerated.mu.Lock() + for k, v := range a.schemaGenerated.GeneratedACLsMap { + _ = fmt.Sprintf("node: %v, NodeDataSerialized: %v\n", k, string(v.Data)) + _ = fmt.Sprintf("node: %v, Hash: %v\n", k, v.Hash) + } + a.schemaGenerated.mu.Unlock() + }() + } + wg.Wait() +} + +func TestExportACLs(t *testing.T) { + const ( + grp_nodes_operators = "grp_nodes_operators" + grp_nodes_ships = "grp_nodes_ships" + grp_commands_commandset1 = "grp_commands_commandset1" + ) + + a := newAuthSchema() + + a.groupNodesAddNode(grp_nodes_operators, "operator1") + a.groupNodesAddNode(grp_nodes_operators, "operator2") + + a.groupNodesAddNode(grp_nodes_ships, "ship100") + a.groupNodesAddNode(grp_nodes_ships, "ship101") + + a.groupCommandsAddCommand(grp_commands_commandset1, "dmesg") + a.groupCommandsAddCommand(grp_commands_commandset1, "date") + + a.aclAdd(grp_nodes_ships, "admin", "useradd -m kongen") + a.aclAdd("ship101", "admin", "HORSE") + + a.aclAdd(grp_nodes_ships, grp_nodes_operators, grp_commands_commandset1) + + js, err := a.exportACLs() + if err != nil { + t.Fatalf("%v", err) + } + + want := `{"grp_nodes_ships":{"admin":{"useradd -m kongen":{}},"grp_nodes_operators":{"grp_commands_commandset1":{}}},"ship101":{"admin":{"HORSE":{}}}}` + + if string(js) != string(want) { + t.Fatalf("error: export does not match with what we want\n") + } +} + +func TestImportACLs(t *testing.T) { + // js := `{"grp_nodes_ships":{"admin":{"useradd -m kongen":{}},"grp_nodes_operators":{"grp_commands_commandset1":{}}},"ship101":{"admin":{"HORSE":{}}}` + + js := []byte{0x7b, 0x22, 0x67, 0x72, 0x70, 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x73, 0x5f, 0x73, 0x68, 0x69, 0x70, 0x73, 0x22, 0x3a, 0x7b, 0x22, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x22, 0x3a, 0x7b, 0x22, 0x75, 0x73, 0x65, 0x72, 0x61, 0x64, 0x64, 0x20, 0x2d, 0x6d, 0x20, 0x6b, 0x6f, 0x6e, 0x67, 0x65, 0x6e, 0x22, 0x3a, 0x7b, 0x7d, 0x7d, 0x2c, 0x22, 0x67, 0x72, 0x70, 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x73, 0x5f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x73, 0x22, 0x3a, 0x7b, 0x22, 0x67, 0x72, 0x70, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x73, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x73, 0x65, 0x74, 0x31, 0x22, 0x3a, 0x7b, 0x7d, 0x7d, 0x7d, 0x2c, 0x22, 0x73, 0x68, 0x69, 0x70, 0x31, 0x30, 0x31, 0x22, 0x3a, 0x7b, 0x22, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x22, 0x3a, 0x7b, 0x22, 0x48, 0x4f, 0x52, 0x53, 0x45, 0x22, 0x3a, 0x7b, 0x7d, 0x7d, 0x7d, 0x7d} + + want := `map[grp_nodes_ships:map[admin:map[useradd -m kongen:{}] grp_nodes_operators:map[grp_commands_commandset1:{}]] ship101:map[admin:map[HORSE:{}]]]` + + a := newAuthSchema() + + err := a.importACLs(js) + if err != nil { + t.Fatalf("%v", err) + } + + if fmt.Sprintf("%v", a.schemaMain.ACLMap) != want { + t.Fatalf("error: import does not match with what we want\n") + } +}