diff --git a/auth/auth.go b/auth/auth.go index 4e617c998..344055337 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -14,6 +14,15 @@ package auth +import ( + "fmt" + "io/ioutil" + "log" + "path/filepath" + + "gopkg.in/yaml.v2" +) + type Auth struct { // Enable AuthZ/AuthN for featurebase server Enable bool `toml:"enable"` @@ -35,4 +44,109 @@ type Auth struct { // Scope URL ScopeURL string `toml:"scope-url"` + + // Permissions file for groups + PermissionsFile string `toml:"permissions"` +} + +type GroupPermissions struct { + Permissions []Permissions `yaml:"group_permissions"` +} + +type Permissions struct { + GroupId string `yaml:"groupId"` + Index string `yaml:"index"` + Permission string `yaml:"permission"` +} + +func ReadPermissionsFile(filePath string) (yamlData []byte) { + filePathAbs, _ := filepath.Abs(filePath) + yamlData, err := ioutil.ReadFile(filePathAbs) + if err != nil { + panic(err) + } + return yamlData +} + +func (p *GroupPermissions) CreatePermissionsStruct(data []byte) { + err := yaml.Unmarshal([]byte(data), &p) + if err != nil { + log.Fatalf("Error %s", err) + } +} + +func GetPermissions(Auth *Auth, groups []map[string]string, index []string) (permission string, err error) { + // read yaml permissions file + yamlData := ReadPermissionsFile(Auth.PermissionsFile) + + // get group permissions + var p GroupPermissions + p.CreatePermissionsStruct(yamlData) + + // check permissions for all groups and index, and return most permissive + return p.ResolvePermissions(groups, index) +} + +func (p *GroupPermissions) ResolvePermissions(groups []map[string]string, index []string) (permission string, err error) { + + // get union of groups the user is part of obtained from identity provider and groups in permissions file + var groupMatch []Permissions + for _, group := range groups { + for i := range p.Permissions { + if group["id"] == p.Permissions[i].GroupId { + groupMatch = append(groupMatch, p.Permissions[i]) + } + } + } + + if len(groupMatch) == 0 { + return "", fmt.Errorf("User is NOT allowed access to FeatureBase") + } + + // check that user's groups have access to the index user want to access + var indexMatch []Permissions + indexCheck := map[string]bool{} + for _, g := range groupMatch { + for _, idx := range index { + if idx == g.Index { + indexMatch = append(indexMatch, g) + indexCheck[idx] = true + } + } + } + + // check that user has access to every index + indexCount := 0 + for _, value := range indexCheck { + if value { + indexCount += 1 + } + } + + if indexCount != len(index) { + return "", fmt.Errorf("User is not allowed access to index: %s", index) + } + + // check permissions for index user has access to + allPermissions := map[string]bool{ + "admin": false, + "write": false, + "read": false, + } + + for _, g := range indexMatch { + if !allPermissions[g.Permission] { + allPermissions[g.Permission] = true + } + } + + if allPermissions["admin"] { + return "admin", error(nil) + } else if allPermissions["write"] { + return "write", error(nil) + } else if allPermissions["read"] { + return "read", error(nil) + } else { + return "", fmt.Errorf("No permissions found") + } } diff --git a/auth/auth_test.go b/auth/auth_test.go new file mode 100644 index 000000000..ddd081e80 --- /dev/null +++ b/auth/auth_test.go @@ -0,0 +1,231 @@ +// Copyright 2017 Pilosa Corp. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package auth_test + +import ( + "fmt" + "reflect" + "strings" + "testing" + + "github.com/molecula/featurebase/v2/auth" +) + +func createStruct(inputs [][]string) (permissions auth.GroupPermissions) { + var sliceStruct []auth.Permissions + for _, i := range inputs { + groupId := i[0] + index := i[1] + permission := i[2] + p := auth.Permissions{groupId, index, permission} + sliceStruct = append(sliceStruct, p) + } + permissions = auth.GroupPermissions{Permissions: sliceStruct} + return permissions +} + +func TestAuth_CreatePermissionsStruct(t *testing.T) { + var singleInput = []byte(`group_permissions: + - group: + groupId: "dca35310-ecda-4f23-86cd-876aee55906b" + index: "test" + permission: "read"`) + + var emptyInput = []byte(`group_permissions: + - group: + groupId: "" + index: "" + permission: ""`) + + var multiInput = []byte(`group_permissions: + - group: + groupId: "dca35310-ecda-4f23-86cd-876aee55906b" + index: "test" + permission: "read" + - group: + groupId: "dca35310-ecda-4f23-86cd-876aee559900" + index: "test" + permission: "admin"`) + + var slice1 [][]string + var slice2 [][]string + var slice3 [][]string + var subslice1 []string + var subslice2 []string + var subslice3 []string + subslice1 = append(subslice1, "dca35310-ecda-4f23-86cd-876aee55906b", "test", "read") + subslice2 = append(subslice2, "", "", "") + subslice3 = append(subslice3, "dca35310-ecda-4f23-86cd-876aee559900", "test", "admin") + slice1 = append(slice1, subslice1) + slice2 = append(slice2, subslice2) + slice3 = append(slice3, subslice1, subslice3) + singleStruct := createStruct(slice1) + emptyStruct := createStruct(slice2) + multiStruct := createStruct(slice3) + + tests := []struct { + input []byte + output auth.GroupPermissions + }{ + {singleInput, singleStruct}, + {emptyInput, emptyStruct}, + {multiInput, multiStruct}, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + var p auth.GroupPermissions + p.CreatePermissionsStruct(test.input) + + if !reflect.DeepEqual(p, test.output) { + t.Fatalf("Expected output %s, but got %s", test.output, p) + } + }, + ) + } +} + +func createGroupMaps(groups []string) []map[string]string { + + var group1 []map[string]string + for _, i := range groups { + map1 := map[string]string{} + map1["id"] = i + group1 = append(group1, map1) + } + return group1 +} + +func TestAuth_ResolvePermissions(t *testing.T) { + // initializes different example of permissions file in yaml + var permissions1 = []byte(`group_permissions: + - group: + groupId: "dca35310-ecda-4f23-86cd-876aee55906b" + index: "test" + permission: "read"`) + + var permissions2 = []byte(`group_permissions: + - group: + groupId: "dca35310-ecda-4f23-86cd-876aee559900" + index: "test" + permission: "read" + - group: + groupId: "dca35310-ecda-4f23-86cd-876aee559900" + index: "test" + permission: "write"`) + + var permissions3 = []byte(`group_permissions: + - group: + groupId: "dca35310-ecda-4f23-86cd-876aee559900" + index: "test" + permission: "read" + - group: + groupId: "dca35310-ecda-4f23-86cd-876aee559900" + index: "test" + permission: "admin"`) + + var permissions4 = []byte(`group_permissions: + - group: + groupId: "dca35310-ecda-4f23-86cd-876aee55906b" + index: "test" + permission: "" + - group: + groupId: "dca35310-ecda-4f23-86cd-876aee559900" + index: "test" + permission: "admin"`) + + // initializes groups that are returned from identity provider + groupsList1 := []string{} + groupsList2 := []string{"dca35310-ecda-4f23-86cd-876aee55906b"} + groupsList3 := []string{"dca35310-ecda-4f23-86cd-876aee55906b", "dca35310-ecda-4f23-86cd-876aee559900"} + + tests := []struct { + permissions []byte + groups []map[string]string + index []string + userAccess string + err string + }{ + { + permissions1, + createGroupMaps(groupsList1), + []string{"test"}, + "", + "User is NOT allowed access to FeatureBase", + }, + { + permissions1, + createGroupMaps(groupsList2), + []string{"test1"}, + "", + "User is not allowed access to index", + }, + { + permissions1, + createGroupMaps(groupsList2), + []string{"test"}, + "read", + "", + }, + { + permissions2, + createGroupMaps(groupsList3), + []string{"test"}, + "write", + "", + }, + { + permissions3, + createGroupMaps(groupsList3), + []string{"test"}, + "admin", + "", + }, + { + permissions2, + createGroupMaps(groupsList3), + []string{"test"}, + "write", + "", + }, + { + permissions4, + createGroupMaps(groupsList2), + []string{"test"}, + "", + "No permissions found", + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + + var p auth.GroupPermissions + p.CreatePermissionsStruct(test.permissions) + + p1, err := p.ResolvePermissions(test.groups, test.index) + + if p1 != test.userAccess { + t.Errorf("Expected permission to be %s, but got %s", test.userAccess, p1) + } + + if err != nil { + if !strings.Contains(err.Error(), test.err) { + t.Errorf("Expected error to contain %s, but got %s", test.err, err.Error()) + } + } + + }) + } +} diff --git a/ctl/server.go b/ctl/server.go index c5d43a937..06f7d1bb2 100644 --- a/ctl/server.go +++ b/ctl/server.go @@ -130,5 +130,5 @@ func BuildServerFlags(cmd *cobra.Command, srv *server.Command) { flags.StringVar(&srv.Config.Auth.TokenURL, "auth.token-url", srv.Config.Auth.TokenURL, "Identity Provider's Token URL.") flags.StringVar(&srv.Config.Auth.GroupEndpointURL, "auth.group-endpoint-url", srv.Config.Auth.GroupEndpointURL, "Identity Provider's Group endpoint URL.") flags.StringVar(&srv.Config.Auth.ScopeURL, "auth.scope-url", srv.Config.Auth.ScopeURL, "Identity Provider's Scope URL.") - + flags.StringVar(&srv.Config.Auth.PermissionsFile, "auth.permissions", srv.Config.Auth.PermissionsFile, "Permissions' file with group authorization.") } diff --git a/install/featurebase.conf b/install/featurebase.conf index 540a410f4..5b1e73345 100644 --- a/install/featurebase.conf +++ b/install/featurebase.conf @@ -380,4 +380,5 @@ log-path = "/var/log/molecula/featurebase.log" # authorize-url = "" # token-url = "" # group-endpoint-url = "" -# scope-url = "" \ No newline at end of file +# scope-url = "" +# permissions = "" \ No newline at end of file diff --git a/server/config.go b/server/config.go index 0c1989c7a..6f74d4f25 100644 --- a/server/config.go +++ b/server/config.go @@ -620,6 +620,7 @@ func (c *Config) ValidateAuth() ([]error, error) { "TokenURL": c.Auth.TokenURL, "GroupEndpointURL": c.Auth.GroupEndpointURL, "ScopeURL": c.Auth.ScopeURL, + "PermissionsFile": c.Auth.PermissionsFile, } errors := make([]error, 0) @@ -636,6 +637,15 @@ func (c *Config) ValidateAuth() ([]error, error) { continue } } + + if strings.Contains(name, "File") { + yamlData := auth.ReadPermissionsFile(value) + var p auth.GroupPermissions + p.CreatePermissionsStruct(yamlData) + if len(p.Permissions) == 0 { + errors = append(errors, fmt.Errorf("No group permissions found in permissions file: %s", value)) + } + } } if len(errors) > 0 { return errors, fmt.Errorf("there were errors validating config")