diff --git a/adapter/adapter.go b/adapter/adapter.go index 73ddf45..edf4238 100644 --- a/adapter/adapter.go +++ b/adapter/adapter.go @@ -453,7 +453,7 @@ func TriggerCommand(ctx context.Context, rawCommand string, id RequestorIdentity "arg": cmdInput.Parameters, } - perms, err := da.UserPermissions(ctx, id.GortUser.Username) + perms, err := da.UserPermissionList(ctx, id.GortUser.Username) if err != nil { da.RequestError(ctx, request, err) telemetry.Errors().WithError(err).Commit(ctx) @@ -463,7 +463,7 @@ func TriggerCommand(ctx context.Context, rawCommand string, id RequestorIdentity return nil, fmt.Errorf("user permission load error: %w", err) } - allowed, err := auth.EvaluateCommandEntry(perms, cmdEntry, env) + allowed, err := auth.EvaluateCommandEntry(perms.Strings(), cmdEntry, env) if err != nil { da.RequestError(ctx, request, err) telemetry.Errors().WithError(err).Commit(ctx) diff --git a/cli/bundle-versions.go b/cli/bundle-versions.go new file mode 100644 index 0000000..7fc0319 --- /dev/null +++ b/cli/bundle-versions.go @@ -0,0 +1,91 @@ +/* + * Copyright 2021 The Gort Authors + * + * 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 cli + +import ( + "fmt" + + "github.com/getgort/gort/client" + "github.com/spf13/cobra" +) + +const ( + bundleVersionsUse = "versions" + bundleVersionsShort = "Lists installed bundle versions." + bundleVersionsLong = "List all versions of an installed bundle." + bundleVersionsUsage = `Usage: gort bundle versions [OPTIONS] NAME + + Lists installed versions of a bundle. + + All versions of the specified bundle are listed, along + with their status ("Enabled", "Disabled", "Incompatible") + + Options: + --help Show this message and exit. +` +) + +// TODO: Support incompatible flag +// -x, --incompatible Lists only incompatible bundle versions + +// GetBundleVersionsCmd is a command +func GetBundleVersionsCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: bundleVersionsUse, + Short: bundleVersionsShort, + Long: bundleVersionsLong, + RunE: bundleVersionsCmd, + Args: cobra.ExactArgs(1), + } + + cmd.SetUsageTemplate(bundleVersionsUsage) + + return cmd +} + +func bundleVersionsCmd(cmd *cobra.Command, args []string) error { + const format = "%-12s%-12s%-12s\n" + + gortClient, err := client.Connect(FlagGortProfile) + if err != nil { + return err + } + + bundles, err := gortClient.BundleListVersions(args[0]) + if err != nil { + return err + } + + fmt.Printf(format, "BUNDLE", "VERSION", "STATUS") + + for _, b := range bundles { + if b.Version == "" { + b.Version = "-" + } + + status := "Disabled" + if b.Enabled { + status = "Enabled" + } + // TODO: Determine whether bundles are incompatible + + fmt.Printf(format, b.Name, b.Version, status) + + } + + return nil +} diff --git a/cli/bundle.go b/cli/bundle.go index 9e45490..5d1ca4b 100644 --- a/cli/bundle.go +++ b/cli/bundle.go @@ -64,6 +64,7 @@ func GetBundleCmd() *cobra.Command { cmd.AddCommand(GetBundleListCmd()) cmd.AddCommand(GetBundleUninstallCmd()) cmd.AddCommand(GetBundleYamlCmd()) + cmd.AddCommand(GetBundleVersionsCmd()) return cmd } diff --git a/cli/hidden-command.go b/cli/hidden-command.go index d81104c..817e64e 100644 --- a/cli/hidden-command.go +++ b/cli/hidden-command.go @@ -30,6 +30,8 @@ const ( hiddenCommandLong = `Provides information about a command. If no command is specified, this will list all commands installed in Gort. + +If a command is specified, this will return information about the specified command. ` hiddenCommandUsage = `Usage: !gort:help [flags] [command] @@ -46,6 +48,7 @@ func GetHiddenCommandCmd() *cobra.Command { Short: hiddenCommandShort, Long: hiddenCommandLong, RunE: hiddenCommandCmd, + Args: cobra.RangeArgs(0, 1), } cmd.SetUsageTemplate(hiddenCommandUsage) @@ -59,6 +62,46 @@ func hiddenCommandCmd(cmd *cobra.Command, args []string) error { return err } + if len(args) == 0 { + return listAllCommands(gortClient) + } + + return detailCommand(gortClient, args[0]) +} + +func detailCommand(gortClient *client.GortClient, command string) error { + bundles, err := gortClient.BundleList() + if err != nil { + return err + } + + var found bool + for _, b := range bundles { + for k := range b.Commands { + cmdName := fmt.Sprintf("%s:%s", b.Name, k) + if cmdName == command || k == command { + fmt.Println(cmdName) + fmt.Println("==") + if len(b.LongDescription) > 0 { + fmt.Println(b.LongDescription) + } else if len(b.Description) > 0 { + fmt.Println(b.Description) + } + fmt.Println() + fmt.Printf("Type `%v --help` for more information.\n", k) + found = true + } + } + } + + if !found { + return fmt.Errorf("command not found: %v", command) + } + + return nil +} + +func listAllCommands(gortClient *client.GortClient) error { bundles, err := gortClient.BundleList() if err != nil { return err @@ -69,7 +112,7 @@ func hiddenCommandCmd(cmd *cobra.Command, args []string) error { cmds := []string{} for _, b := range bundles { - for k, _ := range b.Commands { + for k := range b.Commands { cmds = append(cmds, fmt.Sprintf("- %s:%s", b.Name, k)) } } diff --git a/cli/permission-list.go b/cli/permission-list.go new file mode 100644 index 0000000..8f1aedc --- /dev/null +++ b/cli/permission-list.go @@ -0,0 +1,77 @@ +/* + * Copyright 2021 The Gort Authors + * + * 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 cli + +import ( + "fmt" + + "github.com/getgort/gort/client" + "github.com/spf13/cobra" +) + +const ( + permissionListUse = "list" + permissionListShort = "List all permissions installed" + permissionListLong = "Lists all permissions installed, and their currently enabled version, if any." + permissionListUsage = `Usage: + gort permission list [flags] + +Flags: + -h, --help Show this message and exit + +Global Flags: + -P, --profile string The Gort profile within the config file to use +` +) + +// GetPermissionListCmd is a command +func GetPermissionListCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: permissionListUse, + Short: permissionListShort, + Long: permissionListLong, + RunE: permissionListCmd, + } + + cmd.SetUsageTemplate(permissionListUsage) + + return cmd +} + +func permissionListCmd(cmd *cobra.Command, args []string) error { + const format = "%-12s\n" + + gortClient, err := client.Connect(FlagGortProfile) + if err != nil { + return err + } + + bundles, err := gortClient.BundleList() + if err != nil { + return err + } + + fmt.Printf(format, "NAME") + + for _, b := range bundles { + for _, p := range b.Permissions { + fmt.Printf(format, fmt.Sprintf("%v-%v", b.Name, p)) + } + } + + return nil +} diff --git a/cli/permission.go b/cli/permission.go new file mode 100644 index 0000000..70af56e --- /dev/null +++ b/cli/permission.go @@ -0,0 +1,40 @@ +/* + * Copyright 2021 The Gort Authors + * + * 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 cli + +import ( + "github.com/spf13/cobra" +) + +const ( + permissionUse = "permission" + permissionShort = "Perform operations on permissions" + permissionLong = "Allows you to perform permission administration." +) + +// GetPermissionCmd permission +func GetPermissionCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: permissionUse, + Short: permissionShort, + Long: permissionLong, + } + + cmd.AddCommand(GetPermissionListCmd()) + + return cmd +} diff --git a/cli/role-info.go b/cli/role-info.go new file mode 100644 index 0000000..95f961b --- /dev/null +++ b/cli/role-info.go @@ -0,0 +1,81 @@ +/* + * Copyright 2021 The Gort Authors + * + * 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 cli + +import ( + "fmt" + "strings" + + "github.com/getgort/gort/client" + "github.com/spf13/cobra" +) + +const ( + roleInfoUse = "info" + roleInfoShort = "Retrieve information about an existing role" + roleInfoLong = "Retrieve information about an existing role." + roleInfoUsage = `Usage: + gort role info [flags] role_name [version] + +Flags: + -h, --help Show this message and exit + +Global Flags: + -P, --profile string The Gort profile within the config file to use +` +) + +// GetRoleInfoCmd is a command +func GetRoleInfoCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: roleInfoUse, + Short: roleInfoShort, + Long: roleInfoLong, + RunE: roleInfoCmd, + Args: cobra.ExactArgs(1), + } + + cmd.SetUsageTemplate(roleInfoUsage) + + return cmd +} + +func roleInfoCmd(cmd *cobra.Command, args []string) error { + gortClient, err := client.Connect(FlagGortProfile) + if err != nil { + return err + } + + rolename := args[0] + + role, err := gortClient.RoleGet(rolename) + if err != nil { + return err + } + + const format = `Name %s +Permissions %s +Groups %s +` + + fmt.Printf(format, + role.Name, + strings.Join(role.Permissions.Strings(), ", "), + strings.Join(groupNames(role.Groups), ", ")) + + return nil +} diff --git a/cli/role.go b/cli/role.go index add73f2..a53c77d 100644 --- a/cli/role.go +++ b/cli/role.go @@ -54,6 +54,7 @@ func GetRoleCmd() *cobra.Command { cmd.AddCommand(GetRoleCreateCmd()) cmd.AddCommand(GetRoleDeleteCmd()) cmd.AddCommand(GetRoleGrantCmd()) + cmd.AddCommand(GetRoleInfoCmd()) cmd.AddCommand(GetRoleListCmd()) cmd.AddCommand(GetRoleRevokeCmd()) diff --git a/client/client-group.go b/client/client-group.go index c120ede..2644189 100644 --- a/client/client-group.go +++ b/client/client-group.go @@ -20,6 +20,7 @@ import ( "encoding/json" "fmt" "io/ioutil" + "net/http" "github.com/getgort/gort/data/rest" ) @@ -34,7 +35,7 @@ func (c *GortClient) GroupDelete(groupname string) error { } defer resp.Body.Close() - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { return getResponseError(resp) } @@ -52,9 +53,9 @@ func (c *GortClient) GroupExists(groupname string) (bool, error) { defer resp.Body.Close() switch resp.StatusCode { - case 200: + case http.StatusOK: return true, nil - case 404: + case http.StatusNotFound: return false, nil default: return false, getResponseError(resp) @@ -70,7 +71,7 @@ func (c *GortClient) GroupGet(groupname string) (rest.Group, error) { } defer resp.Body.Close() - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { return rest.Group{}, getResponseError(resp) } @@ -97,7 +98,7 @@ func (c *GortClient) GroupList() ([]rest.Group, error) { } defer resp.Body.Close() - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { return []rest.Group{}, getResponseError(resp) } @@ -124,7 +125,7 @@ func (c *GortClient) GroupMemberAdd(groupname string, username string) error { } defer resp.Body.Close() - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { return getResponseError(resp) } @@ -140,7 +141,7 @@ func (c *GortClient) GroupMemberDelete(groupname string, username string) error } defer resp.Body.Close() - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { return getResponseError(resp) } @@ -156,7 +157,7 @@ func (c *GortClient) GroupMemberList(groupname string) ([]rest.User, error) { } defer resp.Body.Close() - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { return []rest.User{}, getResponseError(resp) } @@ -189,7 +190,7 @@ func (c *GortClient) GroupSave(group rest.Group) error { } defer resp.Body.Close() - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { return getResponseError(resp) } @@ -205,7 +206,7 @@ func (c *GortClient) GroupRoleAdd(groupname string, rolename string) error { } defer resp.Body.Close() - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { return getResponseError(resp) } @@ -221,7 +222,7 @@ func (c *GortClient) GroupRoleDelete(groupname string, rolename string) error { } defer resp.Body.Close() - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { return getResponseError(resp) } @@ -237,7 +238,7 @@ func (c *GortClient) GroupRoleList(groupname string) ([]rest.Role, error) { } defer resp.Body.Close() - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { return []rest.Role{}, getResponseError(resp) } diff --git a/client/client-role.go b/client/client-role.go index edfd5ec..657f938 100644 --- a/client/client-role.go +++ b/client/client-role.go @@ -20,6 +20,7 @@ import ( "encoding/json" "fmt" "io/ioutil" + "net/http" "github.com/getgort/gort/data/rest" ) @@ -34,7 +35,7 @@ func (c *GortClient) RoleDelete(rolename string) error { } defer resp.Body.Close() - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { return getResponseError(resp) } @@ -51,7 +52,7 @@ func (c *GortClient) RoleCreate(rolename string) error { } defer resp.Body.Close() - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { return getResponseError(resp) } @@ -67,7 +68,7 @@ func (c *GortClient) RoleList() ([]rest.Group, error) { } defer resp.Body.Close() - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { return []rest.Group{}, getResponseError(resp) } @@ -96,9 +97,9 @@ func (c *GortClient) RoleExists(rolename string) (bool, error) { defer resp.Body.Close() switch resp.StatusCode { - case 200: + case http.StatusOK: return true, nil - case 404: + case http.StatusNotFound: return false, nil default: return false, getResponseError(resp) @@ -106,30 +107,57 @@ func (c *GortClient) RoleExists(rolename string) (bool, error) { } // RoleGet gets an existing role. -func (c *GortClient) RoleGet(rolename string) (rest.Group, error) { +func (c *GortClient) RoleGet(rolename string) (rest.Role, error) { url := fmt.Sprintf("%s/v2/roles/%s", c.profile.URL.String(), rolename) resp, err := c.doRequest("GET", url, []byte{}) if err != nil { - return rest.Group{}, err + return rest.Role{}, err } defer resp.Body.Close() - if resp.StatusCode != 200 { - return rest.Group{}, getResponseError(resp) + if resp.StatusCode != http.StatusOK { + return rest.Role{}, getResponseError(resp) } body, err := ioutil.ReadAll(resp.Body) if err != nil { - return rest.Group{}, err + return rest.Role{}, err } - group := rest.Group{} - err = json.Unmarshal(body, &group) + role := rest.Role{} + err = json.Unmarshal(body, &role) if err != nil { - return rest.Group{}, err + return rest.Role{}, err } - return group, nil + return role, nil +} + +// RolePermissionList comments to be written... +func (c *GortClient) RolePermissionList(username string) (rest.RolePermissionList, error) { + url := fmt.Sprintf("%s/v2/roles/%s/permissions", c.profile.URL.String(), username) + resp, err := c.doRequest("GET", url, []byte{}) + if err != nil { + return rest.RolePermissionList{}, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return rest.RolePermissionList{}, getResponseError(resp) + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return rest.RolePermissionList{}, err + } + + rpl := rest.RolePermissionList{} + err = json.Unmarshal(body, &rpl) + if err != nil { + return rest.RolePermissionList{}, err + } + + return rpl, nil } // RolePermissionRevoke revokes an existing permission from a role @@ -142,7 +170,7 @@ func (c *GortClient) RolePermissionRevoke(rolename string, bundlename string, pe } defer resp.Body.Close() - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { return getResponseError(resp) } @@ -159,7 +187,7 @@ func (c *GortClient) RolePermissionGrant(rolename string, bundlename string, per } defer resp.Body.Close() - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { return getResponseError(resp) } diff --git a/client/client-user.go b/client/client-user.go index fd86cb7..f62dedc 100644 --- a/client/client-user.go +++ b/client/client-user.go @@ -20,6 +20,7 @@ import ( "encoding/json" "fmt" "io/ioutil" + "net/http" "github.com/getgort/gort/data/rest" ) @@ -34,7 +35,7 @@ func (c *GortClient) UserDelete(username string) error { } defer resp.Body.Close() - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { return getResponseError(resp) } @@ -70,7 +71,7 @@ func (c *GortClient) UserGet(username string) (rest.User, error) { } defer resp.Body.Close() - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { return rest.User{}, getResponseError(resp) } @@ -97,7 +98,7 @@ func (c *GortClient) UserGroupList(username string) ([]rest.Group, error) { } defer resp.Body.Close() - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { return []rest.Group{}, getResponseError(resp) } @@ -124,7 +125,7 @@ func (c *GortClient) UserList() ([]rest.User, error) { } defer resp.Body.Close() - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { return []rest.User{}, getResponseError(resp) } @@ -142,6 +143,33 @@ func (c *GortClient) UserList() ([]rest.User, error) { return users, nil } +// UserPermissionList comments to be written... +func (c *GortClient) UserPermissionList(username string) (rest.RolePermissionList, error) { + url := fmt.Sprintf("%s/v2/users/%s/permissions", c.profile.URL.String(), username) + resp, err := c.doRequest("GET", url, []byte{}) + if err != nil { + return rest.RolePermissionList{}, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return rest.RolePermissionList{}, getResponseError(resp) + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return rest.RolePermissionList{}, err + } + + rpl := rest.RolePermissionList{} + err = json.Unmarshal(body, &rpl) + if err != nil { + return rest.RolePermissionList{}, err + } + + return rpl, nil +} + // UserSave will create or update a user. Note the the key is the username: if // this is called with a user whose username exists that user is updated // (empty fields will not be overwritten); otherwise a new user is created. @@ -159,7 +187,7 @@ func (c *GortClient) UserSave(user rest.User) error { } defer resp.Body.Close() - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { return getResponseError(resp) } diff --git a/cmd-root.go b/cmd-root.go index 533269d..e96155e 100644 --- a/cmd-root.go +++ b/cmd-root.go @@ -45,6 +45,7 @@ func GetRootCmd() *cobra.Command { root.AddCommand(cli.GetRoleCmd()) root.AddCommand(cli.GetUserCmd()) root.AddCommand(cli.GetVersionCmd()) + root.AddCommand(cli.GetPermissionCmd()) root.PersistentFlags().StringVarP(&cli.FlagGortProfile, "profile", "P", "", "The Gort profile within the config file to use") diff --git a/data/bundle.go b/data/bundle.go index d5d0a55..767deec 100644 --- a/data/bundle.go +++ b/data/bundle.go @@ -60,7 +60,7 @@ type BundleDocker struct { // section of the config. type BundleCommand struct { Description string `yaml:",omitempty" json:"description,omitempty"` - Executable []string `yaml:",omitempty" json:"executable,omitempty"` + Executable []string `yaml:",omitempty,flow" json:"executable,omitempty"` Name string `yaml:"-" json:"-"` Rules []string `yaml:",omitempty" json:"rules,omitempty"` } diff --git a/data/rest/group-data.go b/data/rest/group-data.go index 8a7dd29..fa6d928 100644 --- a/data/rest/group-data.go +++ b/data/rest/group-data.go @@ -20,5 +20,6 @@ package rest // Gort controller's REST service. type Group struct { Name string `json:"name,omitempty"` + Roles []Role `json:"roles,omitempty"` Users []User `json:"users,omitempty"` } diff --git a/data/rest/role-data.go b/data/rest/role-data.go index 1ef3e7a..340bc06 100644 --- a/data/rest/role-data.go +++ b/data/rest/role-data.go @@ -20,7 +20,8 @@ import "fmt" type Role struct { Name string - Permissions []RolePermission + Permissions RolePermissionList + Groups []Group } type RolePermission struct { @@ -28,6 +29,18 @@ type RolePermission struct { Permission string } -func (r RolePermission) String() string { - return fmt.Sprintf("%s:%s", r.BundleName, r.Permission) +func (p RolePermission) String() string { + return fmt.Sprintf("%s:%s", p.BundleName, p.Permission) +} + +type RolePermissionList []RolePermission + +func (l RolePermissionList) Strings() []string { + s := make([]string, len(l)) + + for i, p := range l { + s[i] = p.String() + } + + return s } diff --git a/dataaccess/dataaccess.go b/dataaccess/dataaccess.go index 9de554f..bc014ae 100644 --- a/dataaccess/dataaccess.go +++ b/dataaccess/dataaccess.go @@ -45,33 +45,36 @@ type DataAccess interface { BundleExists(ctx context.Context, name string, version string) (bool, error) BundleGet(ctx context.Context, name string, version string) (data.Bundle, error) BundleList(ctx context.Context) ([]data.Bundle, error) - BundleListVersions(ctx context.Context, name string) ([]data.Bundle, error) + BundleVersionList(ctx context.Context, name string) ([]data.Bundle, error) BundleUpdate(ctx context.Context, bundle data.Bundle) error - GroupAddUser(ctx context.Context, groupname string, username string) error GroupCreate(ctx context.Context, group rest.Group) error GroupDelete(ctx context.Context, groupname string) error GroupExists(ctx context.Context, groupname string) (bool, error) GroupGet(ctx context.Context, groupname string) (rest.Group, error) GroupList(ctx context.Context) ([]rest.Group, error) - GroupListRoles(ctx context.Context, groupname string) ([]rest.Role, error) - GroupListUsers(ctx context.Context, groupname string) ([]rest.User, error) - GroupRemoveUser(ctx context.Context, groupname string, username string) error + GroupPermissionList(ctx context.Context, groupname string) (rest.RolePermissionList, error) GroupRoleAdd(ctx context.Context, groupname, rolename string) error GroupRoleDelete(ctx context.Context, groupname, rolename string) error + GroupRoleList(ctx context.Context, groupname string) ([]rest.Role, error) GroupUpdate(ctx context.Context, group rest.Group) error GroupUserAdd(ctx context.Context, groupname string, username string) error GroupUserDelete(ctx context.Context, groupname string, username string) error + GroupUserList(ctx context.Context, groupname string) ([]rest.User, error) RoleCreate(ctx context.Context, rolename string) error RoleDelete(ctx context.Context, rolename string) error RoleGet(ctx context.Context, rolename string) (rest.Role, error) + RoleGroupAdd(ctx context.Context, rolename, groupname string) error + RoleGroupDelete(ctx context.Context, rolename, groupname string) error + RoleGroupExists(ctx context.Context, rolename, groupname string) (bool, error) + RoleGroupList(ctx context.Context, rolename string) ([]rest.Group, error) RoleList(ctx context.Context) ([]rest.Role, error) RoleExists(ctx context.Context, rolename string) (bool, error) - RoleHasPermission(ctx context.Context, rolename, bundlename, permission string) (bool, error) RolePermissionAdd(ctx context.Context, rolename, bundlename, permission string) error RolePermissionDelete(ctx context.Context, rolename, bundlename, permission string) error - RolePermissionList(ctx context.Context, rolename string) ([]rest.RolePermission, error) + RolePermissionExists(ctx context.Context, rolename, bundlename, permission string) (bool, error) + RolePermissionList(ctx context.Context, rolename string) (rest.RolePermissionList, error) TokenEvaluate(ctx context.Context, token string) bool TokenGenerate(ctx context.Context, username string, duration time.Duration) (rest.Token, error) @@ -89,6 +92,7 @@ type DataAccess interface { UserGroupAdd(ctx context.Context, username string, groupname string) error UserGroupDelete(ctx context.Context, username string, groupname string) error UserList(ctx context.Context) ([]rest.User, error) - UserPermissions(ctx context.Context, username string) ([]string, error) + UserPermissionList(ctx context.Context, username string) (rest.RolePermissionList, error) + UserRoleList(ctx context.Context, username string) ([]rest.Role, error) UserUpdate(ctx context.Context, user rest.User) error } diff --git a/dataaccess/memory/bundle-access.go b/dataaccess/memory/bundle-access.go index 9d33500..28b07d1 100644 --- a/dataaccess/memory/bundle-access.go +++ b/dataaccess/memory/bundle-access.go @@ -192,7 +192,7 @@ func (da *InMemoryDataAccess) BundleList(ctx context.Context) ([]data.Bundle, er } // BundleListVersions TBD -func (da *InMemoryDataAccess) BundleListVersions(ctx context.Context, name string) ([]data.Bundle, error) { +func (da *InMemoryDataAccess) BundleVersionList(ctx context.Context, name string) ([]data.Bundle, error) { list := make([]data.Bundle, 0) for _, g := range da.bundles { diff --git a/dataaccess/memory/bundle-access_test.go b/dataaccess/memory/bundle-access_test.go index 0eeb5d7..d145989 100644 --- a/dataaccess/memory/bundle-access_test.go +++ b/dataaccess/memory/bundle-access_test.go @@ -35,7 +35,7 @@ func testBundleAccess(t *testing.T) { t.Run("testBundleDelete", testBundleDelete) t.Run("testBundleGet", testBundleGet) t.Run("testBundleList", testBundleList) - t.Run("testBundleListVersions", testBundleListVersions) + t.Run("testBundleVersionList", testBundleVersionList) t.Run("testFindCommandEntry", testFindCommandEntry) } @@ -340,7 +340,7 @@ func testBundleList(t *testing.T) { } } -func testBundleListVersions(t *testing.T) { +func testBundleVersionList(t *testing.T) { da.BundleCreate(ctx, data.Bundle{GortBundleVersion: 5, Name: "test-list-0", Version: "0.0", Description: "foo"}) defer da.BundleDelete(ctx, "test-list-0", "0.0") da.BundleCreate(ctx, data.Bundle{GortBundleVersion: 5, Name: "test-list-0", Version: "0.1", Description: "foo"}) @@ -350,7 +350,7 @@ func testBundleListVersions(t *testing.T) { da.BundleCreate(ctx, data.Bundle{GortBundleVersion: 5, Name: "test-list-1", Version: "0.1", Description: "foo"}) defer da.BundleDelete(ctx, "test-list-1", "0.1") - bundles, err := da.BundleListVersions(ctx, "test-list-0") + bundles, err := da.BundleVersionList(ctx, "test-list-0") assert.NoError(t, err) if len(bundles) != 2 { diff --git a/dataaccess/memory/group-access.go b/dataaccess/memory/group-access.go index 5415209..b0de32c 100644 --- a/dataaccess/memory/group-access.go +++ b/dataaccess/memory/group-access.go @@ -24,39 +24,6 @@ import ( "github.com/getgort/gort/dataaccess/errs" ) -// GroupAddUser adds a user to a group -func (da *InMemoryDataAccess) GroupAddUser(ctx context.Context, groupname string, username string) error { - if groupname == "" { - return errs.ErrEmptyGroupName - } - - exists, err := da.GroupExists(ctx, groupname) - if err != nil { - return err - } - if !exists { - return errs.ErrNoSuchGroup - } - - if username == "" { - return errs.ErrEmptyUserName - } - - exists, err = da.UserExists(ctx, username) - if err != nil { - return err - } - if !exists { - return errs.ErrNoSuchUser - } - - group := da.groups[groupname] - user := da.users[username] - group.Users = append(group.Users, *user) - - return nil -} - // GroupCreate creates a new user group. func (da *InMemoryDataAccess) GroupCreate(ctx context.Context, group rest.Group) error { if group.Name == "" { @@ -126,32 +93,6 @@ func (da *InMemoryDataAccess) GroupGet(ctx context.Context, groupname string) (r return *group, nil } -// GroupRoleAdd grants one or more roles to a group. -func (da *InMemoryDataAccess) GroupRoleAdd(ctx context.Context, groupname, rolename string) error { - b, err := da.GroupExists(ctx, groupname) - if err != nil { - return err - } else if !b { - return errs.ErrNoSuchGroup - } - - b, err = da.RoleExists(ctx, rolename) - if err != nil { - return err - } else if !b { - return errs.ErrNoSuchRole - } - - m := da.grouproles[groupname] - if m == nil { - m = make(map[string]*rest.Role) - da.grouproles[groupname] = m - } - - m[rolename] = da.roles[rolename] - return nil -} - // GroupList returns a list of all known groups in the datastore. // Passwords are not included. Nice try. func (da *InMemoryDataAccess) GroupList(ctx context.Context) ([]rest.Group, error) { @@ -164,76 +105,91 @@ func (da *InMemoryDataAccess) GroupList(ctx context.Context) ([]rest.Group, erro return list, nil } -func (da *InMemoryDataAccess) GroupListRoles(ctx context.Context, groupname string) ([]rest.Role, error) { - roles := []rest.Role{} - - gr := da.grouproles[groupname] - if gr == nil { - return roles, nil +func (da *InMemoryDataAccess) GroupPermissionList(ctx context.Context, groupname string) (rest.RolePermissionList, error) { + roles, err := da.GroupRoleList(ctx, groupname) + if err != nil { + return rest.RolePermissionList{}, err } - for _, r := range gr { - roles = append(roles, *r) + mp := map[string]rest.RolePermission{} + + for _, r := range roles { + rpl, err := da.RolePermissionList(ctx, r.Name) + if err != nil { + return rest.RolePermissionList{}, err + } + + for _, rp := range rpl { + mp[rp.String()] = rp + } } - sort.Slice(roles, func(i, j int) bool { return roles[i].Name < roles[j].Name }) + pp := []rest.RolePermission{} - return roles, nil -} + for _, p := range mp { + pp = append(pp, p) + } + + sort.Slice(pp, func(i, j int) bool { return pp[i].String() < pp[j].String() }) -func (da *InMemoryDataAccess) GroupListUsers(ctx context.Context, groupname string) ([]rest.User, error) { - return nil, errs.ErrNotImplemented + return pp, nil } -// GroupRemoveUser removes one or more users from a group. -func (da *InMemoryDataAccess) GroupRemoveUser(ctx context.Context, groupname string, username string) error { - if groupname == "" { - return errs.ErrEmptyGroupName +func (da *InMemoryDataAccess) GroupRoleList(ctx context.Context, groupname string) ([]rest.Role, error) { + gr := da.groups[groupname] + if gr == nil { + return []rest.Role{}, nil } - exists, err := da.GroupExists(ctx, groupname) - if err != nil { - return err - } + sort.Slice(gr.Roles, func(i, j int) bool { return gr.Roles[i].Name < gr.Roles[j].Name }) + + return gr.Roles, nil +} + +// GroupRoleAdd grants one or more roles to a group. +func (da *InMemoryDataAccess) GroupRoleAdd(ctx context.Context, groupname, rolename string) error { + group, exists := da.groups[groupname] if !exists { return errs.ErrNoSuchGroup } - group := da.groups[groupname] - - for i, u := range group.Users { - if u.Username == username { - group.Users = append(group.Users[:i], group.Users[i+1:]...) - return nil - } + role, exists := da.roles[rolename] + if !exists { + return errs.ErrNoSuchRole } - return errs.ErrNoSuchUser + group.Roles = append(group.Roles, *role) + role.Groups = append(role.Groups, *group) + + return nil } // GroupRoleDelete revokes one or more roles from a group. func (da *InMemoryDataAccess) GroupRoleDelete(ctx context.Context, groupname, rolename string) error { - b, err := da.GroupExists(ctx, groupname) - if err != nil { - return err - } else if !b { + group, exists := da.groups[groupname] + if !exists { return errs.ErrNoSuchGroup } - b, err = da.RoleExists(ctx, rolename) - if err != nil { - return err - } else if !b { + role, exists := da.roles[rolename] + if !exists { return errs.ErrNoSuchRole } - m := da.grouproles[groupname] - if m == nil { - m = make(map[string]*rest.Role) - da.grouproles[groupname] = m + for i, r := range group.Roles { + if r.Name == rolename { + group.Roles = append(group.Roles[:i], group.Roles[i+1:]...) + break + } + } + + for i, g := range role.Groups { + if g.Name == groupname { + role.Groups = append(role.Groups[:i], role.Groups[i+1:]...) + break + } } - delete(m, rolename) return nil } @@ -258,12 +214,70 @@ func (da *InMemoryDataAccess) GroupUpdate(ctx context.Context, group rest.Group) return nil } -// GroupUserAdd comments TBD -func (da *InMemoryDataAccess) GroupUserAdd(ctx context.Context, group string, user string) error { - return errs.ErrNotImplemented +// GroupUserAdd adds a user to a group +func (da *InMemoryDataAccess) GroupUserAdd(ctx context.Context, groupname string, username string) error { + if groupname == "" { + return errs.ErrEmptyGroupName + } + + exists, err := da.GroupExists(ctx, groupname) + if err != nil { + return err + } + if !exists { + return errs.ErrNoSuchGroup + } + + if username == "" { + return errs.ErrEmptyUserName + } + + exists, err = da.UserExists(ctx, username) + if err != nil { + return err + } + if !exists { + return errs.ErrNoSuchUser + } + + group := da.groups[groupname] + user := da.users[username] + group.Users = append(group.Users, *user) + + return nil +} + +// GroupUserDelete removes one or more users from a group. +func (da *InMemoryDataAccess) GroupUserDelete(ctx context.Context, groupname string, username string) error { + if groupname == "" { + return errs.ErrEmptyGroupName + } + + exists, err := da.GroupExists(ctx, groupname) + if err != nil { + return err + } + if !exists { + return errs.ErrNoSuchGroup + } + + group := da.groups[groupname] + + for i, u := range group.Users { + if u.Username == username { + group.Users = append(group.Users[:i], group.Users[i+1:]...) + return nil + } + } + + return errs.ErrNoSuchUser } -// GroupUserDelete comments TBD -func (da *InMemoryDataAccess) GroupUserDelete(ctx context.Context, group string, user string) error { - return errs.ErrNotImplemented +func (da *InMemoryDataAccess) GroupUserList(ctx context.Context, groupname string) ([]rest.User, error) { + group, exists := da.groups[groupname] + if !exists { + return []rest.User{}, errs.ErrNoSuchGroup + } + + return group.Users, nil } diff --git a/dataaccess/memory/group-access_test.go b/dataaccess/memory/group-access_test.go index a375974..6b6aa42 100644 --- a/dataaccess/memory/group-access_test.go +++ b/dataaccess/memory/group-access_test.go @@ -25,44 +25,85 @@ import ( ) func testGroupAccess(t *testing.T) { - t.Run("testGroupAddUser", testGroupAddUser) + t.Run("testGroupUserAdd", testGroupUserAdd) + t.Run("testGroupUserList", testGroupUserList) t.Run("testGroupCreate", testGroupCreate) t.Run("testGroupDelete", testGroupDelete) t.Run("testGroupExists", testGroupExists) t.Run("testGroupGet", testGroupGet) t.Run("testGroupRoleAdd", testGroupRoleAdd) + t.Run("testGroupPermissionList", testGroupPermissionList) t.Run("testGroupList", testGroupList) - t.Run("testGroupListRoles", testGroupListRoles) - t.Run("testGroupRemoveUser", testGroupRemoveUser) + t.Run("testGroupRoleList", testGroupRoleList) + t.Run("testGroupUserDelete", testGroupUserDelete) } -func testGroupAddUser(t *testing.T) { - err := da.GroupAddUser(ctx, "foo", "bar") +func testGroupUserAdd(t *testing.T) { + var ( + groupname = "group-test-group-user-add" + username = "user-test-group-user-add" + useremail = "user@foo.bar" + ) + + err := da.GroupUserAdd(ctx, groupname, username) assert.Error(t, err, errs.ErrNoSuchGroup) - da.GroupCreate(ctx, rest.Group{Name: "foo"}) - defer da.GroupDelete(ctx, "foo") + da.GroupCreate(ctx, rest.Group{Name: groupname}) + defer da.GroupDelete(ctx, groupname) - err = da.GroupAddUser(ctx, "foo", "bar") + err = da.GroupUserAdd(ctx, groupname, username) assert.Error(t, err, errs.ErrNoSuchUser) - da.UserCreate(ctx, rest.User{Username: "bar", Email: "bar"}) - defer da.UserDelete(ctx, "bar") + da.UserCreate(ctx, rest.User{Username: username, Email: useremail}) + defer da.UserDelete(ctx, username) - err = da.GroupAddUser(ctx, "foo", "bar") + err = da.GroupUserAdd(ctx, groupname, username) assert.NoError(t, err) - group, _ := da.GroupGet(ctx, "foo") + group, _ := da.GroupGet(ctx, groupname) - if len(group.Users) != 1 { - t.Error("Users list empty") + if !assert.Len(t, group.Users, 1) { t.FailNow() } - if len(group.Users) > 0 && group.Users[0].Username != "bar" { - t.Error("Wrong user!") + assert.Equal(t, group.Users[0].Username, username) + assert.Equal(t, group.Users[0].Email, useremail) +} + +func testGroupUserList(t *testing.T) { + var ( + groupname = "group-test-group-user-list" + expected = []rest.User{ + {Username: "user-test-group-user-list-0", Email: "user-test-group-user-list-0@email.com"}, + {Username: "user-test-group-user-list-1", Email: "user-test-group-user-list-1@email.com"}, + } + ) + + _, err := da.GroupUserList(ctx, groupname) + assert.Error(t, err) + assert.ErrorIs(t, err, errs.ErrNoSuchGroup) + + da.GroupCreate(ctx, rest.Group{Name: groupname}) + defer da.GroupDelete(ctx, groupname) + + da.UserCreate(ctx, expected[0]) + defer da.UserDelete(ctx, expected[0].Username) + da.UserCreate(ctx, expected[1]) + defer da.UserDelete(ctx, expected[1].Username) + + da.GroupUserAdd(ctx, groupname, expected[0].Username) + da.GroupUserAdd(ctx, groupname, expected[1].Username) + + actual, err := da.GroupUserList(ctx, groupname) + if !assert.NoError(t, err) { + t.FailNow() + } + + if !assert.Len(t, actual, 2) { t.FailNow() } + + assert.Equal(t, expected, actual) } func testGroupCreate(t *testing.T) { @@ -126,34 +167,74 @@ func testGroupExists(t *testing.T) { } func testGroupGet(t *testing.T) { + groupname := "group-test-group-get" + var err error var group rest.Group // Expect an error _, err = da.GroupGet(ctx, "") - assert.Error(t, err, errs.ErrEmptyGroupName) + assert.ErrorIs(t, err, errs.ErrEmptyGroupName) // Expect an error - _, err = da.GroupGet(ctx, "test-get") - assert.Error(t, err, errs.ErrNoSuchGroup) + _, err = da.GroupGet(ctx, groupname) + assert.ErrorIs(t, err, errs.ErrNoSuchGroup) - da.GroupCreate(ctx, rest.Group{Name: "test-get"}) - defer da.GroupDelete(ctx, "test-get") + da.GroupCreate(ctx, rest.Group{Name: groupname}) + defer da.GroupDelete(ctx, groupname) // da.Group ctx, should exist now - exists, _ := da.GroupExists(ctx, "test-get") - if !exists { - t.Error("Group should exist now") + exists, _ := da.GroupExists(ctx, groupname) + if !assert.True(t, exists) { t.FailNow() } // Expect no error - group, err = da.GroupGet(ctx, "test-get") - assert.NoError(t, err) - if group.Name != "test-get" { - t.Errorf("Group name mismatch: %q is not \"test-get\"", group.Name) + group, err = da.GroupGet(ctx, groupname) + if !assert.NoError(t, err) { + t.FailNow() + } + if !assert.Equal(t, groupname, group.Name) { + t.FailNow() + } +} + +func testGroupPermissionList(t *testing.T) { + const ( + groupname = "group-test-group-permission-list" + rolename = "role-test-group-permission-list" + bundlename = "test" + ) + + var expected = rest.RolePermissionList{ + {BundleName: bundlename, Permission: "role-test-group-permission-list-1"}, + {BundleName: bundlename, Permission: "role-test-group-permission-list-2"}, + {BundleName: bundlename, Permission: "role-test-group-permission-list-3"}, + } + + var err error + + da.GroupCreate(ctx, rest.Group{Name: groupname}) + defer da.GroupDelete(ctx, groupname) + + da.RoleCreate(ctx, rolename) + defer da.RoleDelete(ctx, rolename) + + err = da.GroupRoleAdd(ctx, groupname, rolename) + if !assert.NoError(t, err) { + t.FailNow() + } + + da.RolePermissionAdd(ctx, rolename, expected[0].BundleName, expected[0].Permission) + da.RolePermissionAdd(ctx, rolename, expected[1].BundleName, expected[1].Permission) + da.RolePermissionAdd(ctx, rolename, expected[2].BundleName, expected[2].Permission) + + actual, err := da.GroupPermissionList(ctx, groupname) + if !assert.NoError(t, err) { t.FailNow() } + + assert.Equal(t, expected, actual) } func testGroupRoleAdd(t *testing.T) { @@ -190,7 +271,7 @@ func testGroupRoleAdd(t *testing.T) { }, } - roles, err := da.GroupListRoles(ctx, groupName) + roles, err := da.GroupRoleList(ctx, groupName) if !assert.NoError(t, err) { t.FailNow() } @@ -204,7 +285,7 @@ func testGroupRoleAdd(t *testing.T) { expectedRoles = []rest.Role{} - roles, err = da.GroupListRoles(ctx, groupName) + roles, err = da.GroupRoleList(ctx, groupName) if !assert.NoError(t, err) { t.FailNow() } @@ -238,45 +319,53 @@ func testGroupList(t *testing.T) { } } -func testGroupListRoles(t *testing.T) { - da.GroupCreate(ctx, rest.Group{Name: "group-test-group-list-roles"}) - defer da.GroupDelete(ctx, "group-test-group-list-roles") +func testGroupRoleList(t *testing.T) { + var ( + groupname = "group-test-group-list-roles" + rolenames = []string{ + "role-test-group-list-roles-0", + "role-test-group-list-roles-1", + "role-test-group-list-roles-2", + } + ) + da.GroupCreate(ctx, rest.Group{Name: groupname}) + defer da.GroupDelete(ctx, groupname) - da.RoleCreate(ctx, "role-test-group-list-roles-1") - defer da.RoleDelete(ctx, "role-test-group-list-roles-1") + da.RoleCreate(ctx, rolenames[1]) + defer da.RoleDelete(ctx, rolenames[1]) - da.RoleCreate(ctx, "role-test-group-list-roles-0") - defer da.RoleDelete(ctx, "role-test-group-list-roles-0") + da.RoleCreate(ctx, rolenames[0]) + defer da.RoleDelete(ctx, rolenames[0]) - da.RoleCreate(ctx, "role-test-group-list-roles-2") - defer da.RoleDelete(ctx, "role-test-group-list-roles-2") + da.RoleCreate(ctx, rolenames[2]) + defer da.RoleDelete(ctx, rolenames[2]) - roles, err := da.GroupListRoles(ctx, "group-test-group-list-roles") + roles, err := da.GroupRoleList(ctx, groupname) if !assert.NoError(t, err) && !assert.Empty(t, roles) { t.FailNow() } - err = da.GroupRoleAdd(ctx, "group-test-group-list-roles", "role-test-group-list-roles-1") + err = da.GroupRoleAdd(ctx, groupname, rolenames[1]) if !assert.NoError(t, err) { t.FailNow() } - err = da.GroupRoleAdd(ctx, "group-test-group-list-roles", "role-test-group-list-roles-0") + err = da.GroupRoleAdd(ctx, groupname, rolenames[0]) if !assert.NoError(t, err) { t.FailNow() } - err = da.GroupRoleAdd(ctx, "group-test-group-list-roles", "role-test-group-list-roles-2") + err = da.GroupRoleAdd(ctx, groupname, rolenames[2]) if !assert.NoError(t, err) { t.FailNow() } // Note: alphabetically sorted! expected := []rest.Role{ - {Name: "role-test-group-list-roles-0", Permissions: []rest.RolePermission{}}, - {Name: "role-test-group-list-roles-1", Permissions: []rest.RolePermission{}}, - {Name: "role-test-group-list-roles-2", Permissions: []rest.RolePermission{}}, + {Name: rolenames[0], Permissions: []rest.RolePermission{}}, + {Name: rolenames[1], Permissions: []rest.RolePermission{}}, + {Name: rolenames[2], Permissions: []rest.RolePermission{}}, } - actual, err := da.GroupListRoles(ctx, "group-test-group-list-roles") + actual, err := da.GroupRoleList(ctx, groupname) if !assert.NoError(t, err) { t.FailNow() } @@ -284,14 +373,14 @@ func testGroupListRoles(t *testing.T) { assert.Equal(t, expected, actual) } -func testGroupRemoveUser(t *testing.T) { +func testGroupUserDelete(t *testing.T) { da.GroupCreate(ctx, rest.Group{Name: "foo"}) defer da.GroupDelete(ctx, "foo") da.UserCreate(ctx, rest.User{Username: "bat"}) defer da.UserDelete(ctx, "bat") - err := da.GroupAddUser(ctx, "foo", "bat") + err := da.GroupUserAdd(ctx, "foo", "bat") assert.NoError(t, err) group, err := da.GroupGet(ctx, "foo") @@ -307,7 +396,7 @@ func testGroupRemoveUser(t *testing.T) { t.FailNow() } - err = da.GroupRemoveUser(ctx, "foo", "bat") + err = da.GroupUserDelete(ctx, "foo", "bat") assert.NoError(t, err) group, err = da.GroupGet(ctx, "foo") diff --git a/dataaccess/memory/memory-data-access.go b/dataaccess/memory/memory-data-access.go index f3dd7d4..4be953c 100644 --- a/dataaccess/memory/memory-data-access.go +++ b/dataaccess/memory/memory-data-access.go @@ -26,21 +26,19 @@ import ( // InMemoryDataAccess is an entirely in-memory representation of a data access layer. // Great for testing and development. Terrible for production. type InMemoryDataAccess struct { - bundles map[string]*data.Bundle - groups map[string]*rest.Group - users map[string]*rest.User - roles map[string]*rest.Role - grouproles map[string]map[string]*rest.Role + bundles map[string]*data.Bundle + groups map[string]*rest.Group + users map[string]*rest.User + roles map[string]*rest.Role } // NewInMemoryDataAccess returns a new InMemoryDataAccess instance. func NewInMemoryDataAccess() *InMemoryDataAccess { da := InMemoryDataAccess{ - bundles: make(map[string]*data.Bundle), - groups: make(map[string]*rest.Group), - users: make(map[string]*rest.User), - roles: make(map[string]*rest.Role), - grouproles: make(map[string]map[string]*rest.Role), + bundles: make(map[string]*data.Bundle), + groups: make(map[string]*rest.Group), + users: make(map[string]*rest.User), + roles: make(map[string]*rest.Role), } return &da diff --git a/dataaccess/memory/role-access.go b/dataaccess/memory/role-access.go index 392b406..c416e4f 100644 --- a/dataaccess/memory/role-access.go +++ b/dataaccess/memory/role-access.go @@ -25,30 +25,19 @@ import ( ) // RoleCreate creates a new role. -func (da *InMemoryDataAccess) RoleCreate(ctx context.Context, name string) error { - if name == "" { +func (da *InMemoryDataAccess) RoleCreate(ctx context.Context, rolename string) error { + if rolename == "" { return errs.ErrEmptyRoleName } - if nil != da.roles[name] { + if nil != da.roles[rolename] { return errs.ErrRoleExists } - da.roles[name] = &rest.Role{Name: name, Permissions: []rest.RolePermission{}} + da.roles[rolename] = &rest.Role{Name: rolename, Permissions: []rest.RolePermission{}} return nil } -// RoleList -func (da *InMemoryDataAccess) RoleList(ctx context.Context) ([]rest.Role, error) { - list := make([]rest.Role, 0) - - for _, r := range da.roles { - list = append(list, *r) - } - - return list, nil -} - // RoleDelete func (da *InMemoryDataAccess) RoleDelete(ctx context.Context, name string) error { if name == "" { @@ -73,10 +62,10 @@ func (da *InMemoryDataAccess) RoleExists(ctx context.Context, name string) (bool } // RoleGet gets a specific group. -func (da *InMemoryDataAccess) RoleGet(ctx context.Context, name string) (rest.Role, error) { - role, ok := da.roles[name] +func (da *InMemoryDataAccess) RoleGet(ctx context.Context, rolename string) (rest.Role, error) { + role, ok := da.roles[rolename] - if name == "" { + if rolename == "" { return rest.Role{}, errs.ErrEmptyRoleName } @@ -87,14 +76,80 @@ func (da *InMemoryDataAccess) RoleGet(ctx context.Context, name string) (rest.Ro return *role, nil } -func (da *InMemoryDataAccess) RolePermissionAdd(ctx context.Context, rolename, bundlename, permission string) error { +// RolePermissionExists returns true if the given role has been granted the +// specified permission. It returns an error if rolename is empty or if no +// such role exists. +func (da *InMemoryDataAccess) RolePermissionExists(ctx context.Context, rolename, bundlename, permission string) (bool, error) { + perms, err := da.RolePermissionList(ctx, rolename) + if err != nil { + return false, err + } + + for _, p := range perms { + if p.BundleName == bundlename && p.Permission == permission { + return true, nil + } + } + + return false, nil +} + +// RoleList +func (da *InMemoryDataAccess) RoleList(ctx context.Context) ([]rest.Role, error) { + list := make([]rest.Role, 0) + + for _, r := range da.roles { + list = append(list, *r) + } + + return list, nil +} + +func (da *InMemoryDataAccess) RoleGroupAdd(ctx context.Context, rolename, groupname string) error { + return da.GroupRoleAdd(ctx, groupname, rolename) +} + +func (da *InMemoryDataAccess) RoleGroupDelete(ctx context.Context, rolename, groupname string) error { + return da.GroupRoleDelete(ctx, groupname, rolename) +} + +func (da *InMemoryDataAccess) RoleGroupExists(ctx context.Context, rolename, groupname string) (bool, error) { + groups, err := da.RoleGroupList(ctx, rolename) + if err != nil { + return false, err + } + + if exists, err := da.GroupExists(ctx, groupname); err != nil { + return false, err + } else if !exists { + return false, errs.ErrNoSuchGroup + } + + for _, g := range groups { + if g.Name == groupname { + return true, nil + } + } + + return false, nil +} + +func (da *InMemoryDataAccess) RoleGroupList(ctx context.Context, rolename string) ([]rest.Group, error) { role, ok := da.roles[rolename] + if !ok { + return nil, errs.ErrNoSuchRole + } + return role.Groups, nil +} +func (da *InMemoryDataAccess) RolePermissionAdd(ctx context.Context, rolename, bundlename, permission string) error { + role, ok := da.roles[rolename] if !ok { return errs.ErrNoSuchRole } role.Permissions = append(role.Permissions, rest.RolePermission{BundleName: bundlename, Permission: permission}) + return nil } @@ -119,28 +174,10 @@ func (da *InMemoryDataAccess) RolePermissionDelete(ctx context.Context, rolename return nil } -// RoleHasPermission returns true if the given role has been granted the -// specified permission. It returns an error if rolename is empty or if no -// such role exists. -func (da *InMemoryDataAccess) RoleHasPermission(ctx context.Context, rolename, bundlename, permission string) (bool, error) { - perms, err := da.RolePermissionList(ctx, rolename) - if err != nil { - return false, err - } - - for _, p := range perms { - if p.BundleName == bundlename && p.Permission == permission { - return true, nil - } - } - - return false, nil -} - // RolePermissionList returns returns an alphabetically-sorted list of // fully-qualified (i.e., "bundle:permission") permissions granted to // the role. -func (da *InMemoryDataAccess) RolePermissionList(ctx context.Context, rolename string) ([]rest.RolePermission, error) { +func (da *InMemoryDataAccess) RolePermissionList(ctx context.Context, rolename string) (rest.RolePermissionList, error) { role, err := da.RoleGet(ctx, rolename) if err != nil { return nil, err diff --git a/dataaccess/memory/role-access_test.go b/dataaccess/memory/role-access_test.go index d50b9f1..ff413b9 100644 --- a/dataaccess/memory/role-access_test.go +++ b/dataaccess/memory/role-access_test.go @@ -30,7 +30,12 @@ func testRoleAccess(t *testing.T) { t.Run("testRoleExists", testRoleExists) t.Run("testRoleDelete", testRoleDelete) t.Run("testRoleGet", testRoleGet) - t.Run("testRoleHasPermission", testRoleHasPermission) + t.Run("testRoleGroupAdd", testRoleGroupAdd) + t.Run("testRoleGroupDelete", testRoleGroupDelete) + t.Run("testRoleGroupExists", testRoleGroupExists) + t.Run("testRoleGroupList", testRoleGroupList) + t.Run("testRolePermissionExists", testRolePermissionExists) + t.Run("testRolePermissionAdd", testRolePermissionAdd) t.Run("testRolePermissionList", testRolePermissionList) } @@ -164,13 +169,193 @@ func testRoleGet(t *testing.T) { assert.Equal(t, expected, role) } -func testRoleHasPermission(t *testing.T) { +func testRoleGroupAdd(t *testing.T) { + var err error + + rolename := "role-test-role-group-add" + groupnames := []string{ + "perm-test-role-group-add-0", + "perm-test-role-group-add-1", + } + + // No such group yet + err = da.RoleGroupAdd(ctx, rolename, groupnames[1]) + assert.ErrorIs(t, err, errs.ErrNoSuchGroup) + + da.GroupCreate(ctx, rest.Group{Name: groupnames[0]}) + defer da.GroupDelete(ctx, groupnames[0]) + da.GroupCreate(ctx, rest.Group{Name: groupnames[1]}) + defer da.GroupDelete(ctx, groupnames[1]) + + // Groups exist now, but the role doesn't + err = da.RoleGroupAdd(ctx, rolename, groupnames[1]) + assert.ErrorIs(t, err, errs.ErrNoSuchRole) + + da.RoleCreate(ctx, rolename) + defer da.RoleDelete(ctx, rolename) + + for _, groupname := range groupnames { + err = da.RoleGroupAdd(ctx, rolename, groupname) + assert.NoError(t, err) + } + + for _, groupname := range groupnames { + exists, _ := da.RoleGroupExists(ctx, rolename, groupname) + assert.True(t, exists, groupname) + } +} + +func testRoleGroupDelete(t *testing.T) { + +} + +func testRoleGroupExists(t *testing.T) { + var err error + + rolename := "role-test-role-group-exists" + groupnames := []string{ + "group-test-role-group-exists-0", + "group-test-role-group-exists-1", + } + groupnull := "group-test-role-group-exists-null" + + // No such role yet + _, err = da.RoleGroupExists(ctx, rolename, groupnames[1]) + assert.ErrorIs(t, err, errs.ErrNoSuchRole) + + da.RoleCreate(ctx, rolename) + defer da.RoleDelete(ctx, rolename) + + // Groups exist now, but the role doesn't + _, err = da.RoleGroupExists(ctx, rolename, groupnames[1]) + assert.ErrorIs(t, err, errs.ErrNoSuchGroup) + + da.GroupCreate(ctx, rest.Group{Name: groupnames[0]}) + defer da.GroupDelete(ctx, groupnames[0]) + da.GroupCreate(ctx, rest.Group{Name: groupnames[1]}) + defer da.GroupDelete(ctx, groupnames[1]) + da.GroupCreate(ctx, rest.Group{Name: groupnull}) + defer da.GroupDelete(ctx, groupnull) + + for _, groupname := range groupnames { + da.RoleGroupAdd(ctx, rolename, groupname) + } + + for _, groupname := range groupnames { + exists, err := da.RoleGroupExists(ctx, rolename, groupname) + assert.NoError(t, err) + assert.True(t, exists) + } + + // Null group should NOT exist on the role + exists, err := da.RoleGroupExists(ctx, rolename, groupnull) + assert.NoError(t, err) + assert.False(t, exists) +} + +func testRoleGroupList(t *testing.T) { + var err error + + rolename := "role-test-role-group-list" + groupnames := []string{ + "group-test-role-group-list-0", + "group-test-role-group-list-1", + } + groupnull := "group-test-role-group-list-null" + + // No such role yet + _, err = da.RoleGroupList(ctx, rolename) + assert.ErrorIs(t, err, errs.ErrNoSuchRole) + + da.RoleCreate(ctx, rolename) + defer da.RoleDelete(ctx, rolename) + + // Groups exist now, but the role doesn't + groups, err := da.RoleGroupList(ctx, rolename) + assert.NoError(t, err) + assert.Empty(t, groups) + + da.GroupCreate(ctx, rest.Group{Name: groupnames[1]}) + defer da.GroupDelete(ctx, groupnames[1]) + da.GroupCreate(ctx, rest.Group{Name: groupnames[0]}) + defer da.GroupDelete(ctx, groupnames[0]) + da.GroupCreate(ctx, rest.Group{Name: groupnull}) + defer da.GroupDelete(ctx, groupnull) + + for _, groupname := range groupnames { + da.RoleGroupAdd(ctx, rolename, groupname) + } + + // Currently the groups are NOT expected to be fully described (i.e., + // their roles slices don't have to be complete). + groups, err = da.RoleGroupList(ctx, rolename) + assert.NoError(t, err) + assert.Len(t, groups, 2) + + for i, g := range groups { + assert.Equal(t, groupnames[i], g.Name) + } +} + +func testRolePermissionAdd(t *testing.T) { + var exists bool + var err error + + const rolename = "role-test-role-permission-add" + const bundlename = "test" + const permname1 = "perm-test-role-permission-add-0" + const permname2 = "perm-test-role-permission-add-1" + + da.RoleCreate(ctx, rolename) + defer da.RoleDelete(ctx, rolename) + + role, _ := da.RoleGet(ctx, rolename) + if !assert.Len(t, role.Permissions, 0) { + t.FailNow() + } + + // First permission + err = da.RolePermissionAdd(ctx, rolename, bundlename, permname1) + if !assert.NoError(t, err) { + t.FailNow() + } + defer da.RolePermissionDelete(ctx, rolename, bundlename, permname1) + + role, _ = da.RoleGet(ctx, rolename) + if !assert.Len(t, role.Permissions, 1) { + t.FailNow() + } + + exists, _ = da.RolePermissionExists(ctx, rolename, bundlename, permname1) + if !assert.True(t, exists) { + t.FailNow() + } + + // Second permission + err = da.RolePermissionAdd(ctx, rolename, bundlename, permname2) + if !assert.NoError(t, err) { + t.FailNow() + } + defer da.RolePermissionDelete(ctx, rolename, bundlename, permname2) + + role, _ = da.RoleGet(ctx, rolename) + if !assert.Len(t, role.Permissions, 2) { + t.FailNow() + } + + exists, _ = da.RolePermissionExists(ctx, rolename, bundlename, permname2) + if !assert.True(t, exists) { + t.FailNow() + } +} + +func testRolePermissionExists(t *testing.T) { var err error da.RoleCreate(ctx, "role-test-role-has-permission") defer da.RoleDelete(ctx, "role-test-role-has-permission") - has, err := da.RoleHasPermission(ctx, "role-test-role-has-permission", "test", "permission-test-role-has-permission-1") + has, err := da.RolePermissionExists(ctx, "role-test-role-has-permission", "test", "permission-test-role-has-permission-1") if !assert.NoError(t, err) || !assert.False(t, has) { t.FailNow() } @@ -181,12 +366,12 @@ func testRoleHasPermission(t *testing.T) { } defer da.RolePermissionDelete(ctx, "role-test-role-has-permission", "test", "permission-test-role-has-permission-1") - has, err = da.RoleHasPermission(ctx, "role-test-role-has-permission", "test", "permission-test-role-has-permission-1") + has, err = da.RolePermissionExists(ctx, "role-test-role-has-permission", "test", "permission-test-role-has-permission-1") if !assert.NoError(t, err) || !assert.True(t, has) { t.FailNow() } - has, err = da.RoleHasPermission(ctx, "role-test-role-has-permission", "test", "permission-test-role-has-permission-2") + has, err = da.RolePermissionExists(ctx, "role-test-role-has-permission", "test", "permission-test-role-has-permission-2") if !assert.NoError(t, err) || !assert.False(t, has) { t.FailNow() } @@ -217,7 +402,7 @@ func testRolePermissionList(t *testing.T) { defer da.RolePermissionDelete(ctx, "role-test-role-permission-list", "test", "permission-test-role-permission-list-2") // Expect a sorted list! - expect := []rest.RolePermission{ + expect := rest.RolePermissionList{ {BundleName: "test", Permission: "permission-test-role-permission-list-1"}, {BundleName: "test", Permission: "permission-test-role-permission-list-2"}, {BundleName: "test", Permission: "permission-test-role-permission-list-3"}, diff --git a/dataaccess/memory/user-access.go b/dataaccess/memory/user-access.go index c52e72f..778cd0d 100644 --- a/dataaccess/memory/user-access.go +++ b/dataaccess/memory/user-access.go @@ -128,6 +128,33 @@ func (da *InMemoryDataAccess) UserGetByEmail(ctx context.Context, email string) return rest.User{}, errs.ErrNoSuchUser } +// UserGroupList returns a slice of Group values representing the specified user's group memberships. +// The groups' Users slice is never populated, and is always nil. +func (da *InMemoryDataAccess) UserGroupList(ctx context.Context, username string) ([]rest.Group, error) { + groups := make([]rest.Group, 0) + + for _, group := range da.groups { + for _, user := range group.Users { + if user.Username == username { + groups = append(groups, rest.Group{Name: group.Name}) + continue + } + } + } + + return groups, nil +} + +// UserGroupAdd comments TBD +func (da *InMemoryDataAccess) UserGroupAdd(ctx context.Context, username string, groupname string) error { + return da.GroupUserAdd(ctx, groupname, username) +} + +// UserGroupDelete comments TBD +func (da *InMemoryDataAccess) UserGroupDelete(ctx context.Context, username string, groupname string) error { + return da.GroupUserDelete(ctx, groupname, username) +} + // UserList returns a list of all known users in the datastore. // Passwords are not included. Nice try. func (da *InMemoryDataAccess) UserList(ctx context.Context) ([]rest.User, error) { @@ -141,34 +168,74 @@ func (da *InMemoryDataAccess) UserList(ctx context.Context) ([]rest.User, error) return list, nil } -// UserPermissions returns an alphabetically-sorted list of fully-qualified -// (i.e., "bundle:permission") permissions available to the specified user. -func (da *InMemoryDataAccess) UserPermissions(ctx context.Context, username string) ([]string, error) { - pp := []string{} +// UserPermissionList returns an alphabetically-sorted list of permissions +// available to the specified user. +func (da *InMemoryDataAccess) UserPermissionList(ctx context.Context, username string) (rest.RolePermissionList, error) { + mp := map[string]rest.RolePermission{} + // Permissions aren't attached to users: they're attached to roles, which + // are attached to groups. groups, err := da.UserGroupList(ctx, username) if err != nil { return nil, err } + // Collect all permissions from all groups to remove any repeats. for _, group := range groups { - roles, err := da.GroupListRoles(ctx, group.Name) + gpl, err := da.GroupPermissionList(ctx, group.Name) if err != nil { return nil, err } - for _, role := range roles { - for _, p := range role.Permissions { - pp = append(pp, p.BundleName+":"+p.Permission) - } + for _, p := range gpl { + mp[p.String()] = p } } - sort.Strings(pp) + pp := []rest.RolePermission{} + + for _, p := range mp { + pp = append(pp, p) + } + + sort.Slice(pp, func(i, j int) bool { return pp[i].String() < pp[j].String() }) return pp, nil } +// UserRoleList returns a slice of Role values representing the specified +// user's indirect roles (indirect because users are members of groups, +// and groups have roles). +func (da *InMemoryDataAccess) UserRoleList(ctx context.Context, username string) ([]rest.Role, error) { + rm := map[string]rest.Role{} + + groups, err := da.UserGroupList(ctx, username) + if err != nil { + return []rest.Role{}, err + } + + for _, gr := range groups { + rl, err := da.GroupRoleList(ctx, gr.Name) + if err != nil { + return []rest.Role{}, err + } + + for _, r := range rl { + rm[r.Name] = r + } + } + + roles := []rest.Role{} + + for _, r := range rm { + roles = append(roles, r) + } + + sort.Slice(roles, func(i, j int) bool { return roles[i].Name < roles[j].Name }) + + return roles, nil +} + // UserUpdate is used to update an existing user. An error is returned if the // username is empty or if the user doesn't exist. // TODO Should we let this create users that don't exist? @@ -189,30 +256,3 @@ func (da *InMemoryDataAccess) UserUpdate(ctx context.Context, user rest.User) er return nil } - -// UserGroupList returns a slice of Group values representing the specified user's group memberships. -// The groups' Users slice is never populated, and is always nil. -func (da *InMemoryDataAccess) UserGroupList(ctx context.Context, username string) ([]rest.Group, error) { - groups := make([]rest.Group, 0) - - for _, group := range da.groups { - for _, user := range group.Users { - if user.Username == username { - groups = append(groups, rest.Group{Name: group.Name}) - continue - } - } - } - - return groups, nil -} - -// UserGroupAdd comments TBD -func (da *InMemoryDataAccess) UserGroupAdd(ctx context.Context, username string, groupname string) error { - return da.GroupAddUser(ctx, groupname, username) -} - -// UserGroupDelete comments TBD -func (da *InMemoryDataAccess) UserGroupDelete(ctx context.Context, username string, groupname string) error { - return da.GroupUserDelete(ctx, groupname, username) -} diff --git a/dataaccess/memory/user-access_test.go b/dataaccess/memory/user-access_test.go index 0d96b9a..82a402d 100644 --- a/dataaccess/memory/user-access_test.go +++ b/dataaccess/memory/user-access_test.go @@ -33,7 +33,7 @@ func testUserAccess(t *testing.T) { t.Run("testUserGroupList", testUserGroupList) t.Run("testUserList", testUserList) t.Run("testUserNotExists", testUserNotExists) - t.Run("testUserPermissions", testUserPermissions) + t.Run("testUserPermissionList", testUserPermissionList) t.Run("testUserUpdate", testUserUpdate) } @@ -179,7 +179,7 @@ func testUserGroupList(t *testing.T) { da.UserCreate(ctx, rest.User{Username: "user-test-user-group-list"}) defer da.UserDelete(ctx, "user-test-user-group-list") - da.GroupAddUser(ctx, "group-test-user-group-list-0", "user-test-user-group-list") + da.GroupUserAdd(ctx, "group-test-user-group-list-0", "user-test-user-group-list") expected := []rest.Group{{Name: "group-test-user-group-list-0", Users: nil}} @@ -238,7 +238,8 @@ func testUserNotExists(t *testing.T) { t.FailNow() } } -func testUserPermissions(t *testing.T) { + +func testUserPermissionList(t *testing.T) { var err error err = da.GroupCreate(ctx, rest.Group{Name: "test-perms"}) @@ -252,7 +253,7 @@ func testUserPermissions(t *testing.T) { if !assert.NoError(t, err) { t.FailNow() } - err = da.GroupAddUser(ctx, "test-perms", "test-perms") + err = da.GroupUserAdd(ctx, "test-perms", "test-perms") if !assert.NoError(t, err) { t.FailNow() } @@ -283,12 +284,12 @@ func testUserPermissions(t *testing.T) { // Expected: a sorted list of strings expected := []string{"test:test-perms-0", "test:test-perms-1", "test:test-perms-2"} - actual, err := da.UserPermissions(ctx, "test-perms") + actual, err := da.UserPermissionList(ctx, "test-perms") if !assert.NoError(t, err) { t.FailNow() } - assert.Equal(t, expected, actual) + assert.Equal(t, expected, actual.Strings()) } func testUserUpdate(t *testing.T) { @@ -307,7 +308,7 @@ func testUserUpdate(t *testing.T) { // Get the user we just added. Emails should match. user1, _ := da.UserGet(ctx, "test-update") if userA.Email != user1.Email { - t.Errorf("Email mistatch: %q vs %q", userA.Email, user1.Email) + t.Errorf("Email mismatch: %q vs %q", userA.Email, user1.Email) t.FailNow() } @@ -319,7 +320,7 @@ func testUserUpdate(t *testing.T) { // Get the user we just updated. Emails should match. user2, _ := da.UserGet(ctx, "test-update") if userB.Email != user2.Email { - t.Errorf("Email mistatch: %q vs %q", userB.Email, user2.Email) + t.Errorf("Email mismatch: %q vs %q", userB.Email, user2.Email) t.FailNow() } } diff --git a/dataaccess/postgres/bundle-access.go b/dataaccess/postgres/bundle-access.go index 2d3bf11..12e34f1 100644 --- a/dataaccess/postgres/bundle-access.go +++ b/dataaccess/postgres/bundle-access.go @@ -364,10 +364,63 @@ func (da PostgresDataAccess) BundleList(ctx context.Context) ([]data.Bundle, err return bundles, nil } -// BundleListVersions TBD -func (da PostgresDataAccess) BundleListVersions(ctx context.Context, name string) ([]data.Bundle, error) { +// BundleUpdate TBD +func (da PostgresDataAccess) BundleUpdate(ctx context.Context, bundle data.Bundle) error { + tr := otel.GetTracerProvider().Tracer(telemetry.ServiceName) + ctx, sp := tr.Start(ctx, "postgres.BundleUpdate") + defer sp.End() + + if bundle.Name == "" { + return errs.ErrEmptyBundleName + } + + if bundle.Version == "" { + return errs.ErrEmptyBundleVersion + } + + db, err := da.connect(ctx, "gort") + if err != nil { + return err + } + defer db.Close() + + tx, err := db.BeginTx(ctx, &sql.TxOptions{}) + if err != nil { + return gerr.Wrap(errs.ErrDataAccess, err) + } + + exists, err := da.doBundleExists(ctx, tx, bundle.Name, bundle.Version) + if err != nil { + return err + } else if !exists { + return errs.ErrNoSuchBundle + } + + err = da.doBundleDelete(ctx, tx, bundle.Name, bundle.Version) + if err != nil { + tx.Rollback() + return err + } + + err = da.doBundleInsert(ctx, tx, bundle) + if err != nil { + tx.Rollback() + return err + } + + err = tx.Commit() + if err != nil { + tx.Rollback() + return gerr.Wrap(errs.ErrDataAccess, err) + } + + return nil +} + +// BundleVersionList TBD +func (da PostgresDataAccess) BundleVersionList(ctx context.Context, name string) ([]data.Bundle, error) { tr := otel.GetTracerProvider().Tracer(telemetry.ServiceName) - ctx, sp := tr.Start(ctx, "postgres.BundleListVersions") + ctx, sp := tr.Start(ctx, "postgres.BundleVersionList") defer sp.End() // This is hacky as fuck. I know. @@ -421,53 +474,6 @@ func (da PostgresDataAccess) BundleListVersions(ctx context.Context, name string return bundles, nil } -// BundleUpdate TBD -func (da PostgresDataAccess) BundleUpdate(ctx context.Context, bundle data.Bundle) error { - tr := otel.GetTracerProvider().Tracer(telemetry.ServiceName) - ctx, sp := tr.Start(ctx, "postgres.BundleUpdate") - defer sp.End() - - if bundle.Name == "" { - return errs.ErrEmptyBundleName - } - - if bundle.Version == "" { - return errs.ErrEmptyBundleVersion - } - - db, err := da.connect(ctx, "gort") - if err != nil { - return err - } - defer db.Close() - - tx, err := db.BeginTx(ctx, &sql.TxOptions{}) - if err != nil { - return gerr.Wrap(errs.ErrDataAccess, err) - } - - exists, err := da.doBundleExists(ctx, tx, bundle.Name, bundle.Version) - if err != nil { - return err - } else if !exists { - return errs.ErrNoSuchBundle - } - - err = da.doBundleDelete(ctx, tx, bundle.Name, bundle.Version) - if err != nil { - tx.Rollback() - return err - } - - err = da.doBundleInsert(ctx, tx, bundle) - if err != nil { - tx.Rollback() - return err - } - - return nil -} - // FindCommandEntry is used to find the enabled commands with the provided // bundle and command names. If either is empty, it is treated as a wildcard. // Importantly, this must only return ENABLED commands! diff --git a/dataaccess/postgres/bundle-access_test.go b/dataaccess/postgres/bundle-access_test.go index 364d947..f19ed11 100644 --- a/dataaccess/postgres/bundle-access_test.go +++ b/dataaccess/postgres/bundle-access_test.go @@ -35,7 +35,7 @@ func testBundleAccess(t *testing.T) { t.Run("testBundleDelete", testBundleDelete) t.Run("testBundleGet", testBundleGet) t.Run("testBundleList", testBundleList) - t.Run("testBundleListVersions", testBundleListVersions) + t.Run("testBundleVersionList", testBundleVersionList) t.Run("testFindCommandEntry", testFindCommandEntry) } @@ -340,7 +340,7 @@ func testBundleList(t *testing.T) { } } -func testBundleListVersions(t *testing.T) { +func testBundleVersionList(t *testing.T) { da.BundleCreate(ctx, data.Bundle{GortBundleVersion: 5, Name: "test-list-0", Version: "0.0", Description: "foo"}) defer da.BundleDelete(ctx, "test-list-0", "0.0") da.BundleCreate(ctx, data.Bundle{GortBundleVersion: 5, Name: "test-list-0", Version: "0.1", Description: "foo"}) @@ -350,7 +350,7 @@ func testBundleListVersions(t *testing.T) { da.BundleCreate(ctx, data.Bundle{GortBundleVersion: 5, Name: "test-list-1", Version: "0.1", Description: "foo"}) defer da.BundleDelete(ctx, "test-list-1", "0.1") - bundles, err := da.BundleListVersions(ctx, "test-list-0") + bundles, err := da.BundleVersionList(ctx, "test-list-0") assert.NoError(t, err) if len(bundles) != 2 { diff --git a/dataaccess/postgres/group-access.go b/dataaccess/postgres/group-access.go index 90b0e9b..3ef12ec 100644 --- a/dataaccess/postgres/group-access.go +++ b/dataaccess/postgres/group-access.go @@ -18,6 +18,8 @@ package postgres import ( "context" + "database/sql" + "sort" "go.opentelemetry.io/otel" @@ -27,51 +29,6 @@ import ( "github.com/getgort/gort/telemetry" ) -// GroupAddUser adds a user to a group -func (da PostgresDataAccess) GroupAddUser(ctx context.Context, groupname string, username string) error { - tr := otel.GetTracerProvider().Tracer(telemetry.ServiceName) - ctx, sp := tr.Start(ctx, "postgres.GroupAddUser") - defer sp.End() - - if groupname == "" { - return errs.ErrEmptyGroupName - } - - exists, err := da.GroupExists(ctx, groupname) - if err != nil { - return err - } - if !exists { - return errs.ErrNoSuchGroup - } - - if username == "" { - return errs.ErrEmptyUserName - } - - exists, err = da.UserExists(ctx, username) - if err != nil { - return err - } - if !exists { - return errs.ErrNoSuchUser - } - - db, err := da.connect(ctx, DatabaseGort) - if err != nil { - return err - } - defer db.Close() - - query := `INSERT INTO groupusers (groupname, username) VALUES ($1, $2);` - _, err = db.ExecContext(ctx, query, groupname, username) - if err != nil { - err = gerr.Wrap(errs.ErrDataAccess, err) - } - - return err -} - // GroupCreate creates a new user group. func (da PostgresDataAccess) GroupCreate(ctx context.Context, group rest.Group) error { tr := otel.GetTracerProvider().Tracer(telemetry.ServiceName) @@ -195,11 +152,13 @@ func (da PostgresDataAccess) GroupGet(ctx context.Context, groupname string) (re group := rest.Group{} err = db.QueryRowContext(ctx, query, groupname).Scan(&group.Name) - if err != nil { - return group, gerr.Wrap(errs.ErrNoSuchGroup, err) + if err == sql.ErrNoRows { + return group, errs.ErrNoSuchGroup + } else if err != nil { + return group, gerr.Wrap(errs.ErrDataAccess, err) } - users, err := da.GroupListUsers(ctx, groupname) + users, err := da.GroupUserList(ctx, groupname) if err != nil { return group, err } @@ -209,6 +168,75 @@ func (da PostgresDataAccess) GroupGet(ctx context.Context, groupname string) (re return group, nil } +// GroupList returns a list of all known groups in the datastore. +// Passwords are not included. Nice try. +func (da PostgresDataAccess) GroupList(ctx context.Context) ([]rest.Group, error) { + tr := otel.GetTracerProvider().Tracer(telemetry.ServiceName) + ctx, sp := tr.Start(ctx, "postgres.GroupList") + defer sp.End() + + groups := make([]rest.Group, 0) + + db, err := da.connect(ctx, DatabaseGort) + if err != nil { + return groups, err + } + defer db.Close() + + query := `SELECT groupname FROM groups` + rows, err := db.QueryContext(ctx, query) + if err != nil { + return groups, gerr.Wrap(errs.ErrDataAccess, err) + } + + for rows.Next() { + group := rest.Group{} + + err = rows.Scan(&group.Name) + if err != nil { + return groups, gerr.Wrap(errs.ErrNoSuchGroup, err) + } + + groups = append(groups, group) + } + + return groups, nil +} + +func (da PostgresDataAccess) GroupPermissionList(ctx context.Context, groupname string) (rest.RolePermissionList, error) { + tr := otel.GetTracerProvider().Tracer(telemetry.ServiceName) + ctx, sp := tr.Start(ctx, "postgres.GroupPermissionList") + defer sp.End() + + roles, err := da.GroupRoleList(ctx, groupname) + if err != nil { + return rest.RolePermissionList{}, err + } + + mp := map[string]rest.RolePermission{} + + for _, r := range roles { + rpl, err := da.RolePermissionList(ctx, r.Name) + if err != nil { + return rest.RolePermissionList{}, err + } + + for _, rp := range rpl { + mp[rp.String()] = rp + } + } + + pp := []rest.RolePermission{} + + for _, p := range mp { + pp = append(pp, p) + } + + sort.Slice(pp, func(i, j int) bool { return pp[i].String() < pp[j].String() }) + + return pp, nil +} + // GroupRoleAdd grants one or more roles to a group. func (da PostgresDataAccess) GroupRoleAdd(ctx context.Context, groupname, rolename string) error { tr := otel.GetTracerProvider().Tracer(telemetry.ServiceName) @@ -251,44 +279,39 @@ func (da PostgresDataAccess) GroupRoleAdd(ctx context.Context, groupname, rolena return err } -// GroupList returns a list of all known groups in the datastore. -// Passwords are not included. Nice try. -func (da PostgresDataAccess) GroupList(ctx context.Context) ([]rest.Group, error) { +// GroupRoleDelete revokes a role from a group. +func (da PostgresDataAccess) GroupRoleDelete(ctx context.Context, groupname, rolename string) error { tr := otel.GetTracerProvider().Tracer(telemetry.ServiceName) - ctx, sp := tr.Start(ctx, "postgres.GroupList") + ctx, sp := tr.Start(ctx, "postgres.GroupRoleDelete") defer sp.End() - groups := make([]rest.Group, 0) + if groupname == "" { + return errs.ErrEmptyGroupName + } + + if rolename == "" { + return errs.ErrEmptyRoleName + } db, err := da.connect(ctx, DatabaseGort) if err != nil { - return groups, err + return err } defer db.Close() - query := `SELECT groupname FROM groups` - rows, err := db.QueryContext(ctx, query) + query := `DELETE FROM group_roles + WHERE group_name=$1 AND role_name=$2;` + _, err = db.ExecContext(ctx, query, groupname, rolename) if err != nil { - return groups, gerr.Wrap(errs.ErrDataAccess, err) - } - - for rows.Next() { - group := rest.Group{} - - err = rows.Scan(&group.Name) - if err != nil { - return groups, gerr.Wrap(errs.ErrNoSuchGroup, err) - } - - groups = append(groups, group) + return gerr.Wrap(errs.ErrDataAccess, err) } - return groups, nil + return err } -func (da PostgresDataAccess) GroupListRoles(ctx context.Context, groupname string) ([]rest.Role, error) { +func (da PostgresDataAccess) GroupRoleList(ctx context.Context, groupname string) ([]rest.Role, error) { tr := otel.GetTracerProvider().Tracer(telemetry.ServiceName) - ctx, sp := tr.Start(ctx, "postgres.GroupListRoles") + ctx, sp := tr.Start(ctx, "postgres.GroupRoleList") defer sp.End() if groupname == "" { @@ -326,7 +349,7 @@ func (da PostgresDataAccess) GroupListRoles(ctx context.Context, groupname strin err = rows.Scan(&name) if err != nil { - return nil, gerr.Wrap(errs.ErrNoSuchUser, err) + return nil, gerr.Wrap(errs.ErrDataAccess, err) } role, err := da.RoleGet(ctx, name) @@ -340,51 +363,49 @@ func (da PostgresDataAccess) GroupListRoles(ctx context.Context, groupname strin return roles, nil } -// GroupListUsers returns a list of all known users in a group. -func (da PostgresDataAccess) GroupListUsers(ctx context.Context, groupname string) ([]rest.User, error) { +// GroupUpdate is used to update an existing group. An error is returned if the +// groupname is empty or if the group doesn't exist. +// TODO Should we let this create groups that don't exist? +func (da PostgresDataAccess) GroupUpdate(ctx context.Context, group rest.Group) error { tr := otel.GetTracerProvider().Tracer(telemetry.ServiceName) - ctx, sp := tr.Start(ctx, "postgres.GroupListUsers") + ctx, sp := tr.Start(ctx, "postgres.GroupUpdate") defer sp.End() - users := make([]rest.User, 0) + if group.Name == "" { + return errs.ErrEmptyGroupName + } - db, err := da.connect(ctx, DatabaseGort) + exists, err := da.UserExists(ctx, group.Name) if err != nil { - return users, err + return err + } + if !exists { + return errs.ErrNoSuchGroup } - defer db.Close() - - query := `SELECT email, full_name, username - FROM users - WHERE username IN ( - SELECT username - FROM groupusers - WHERE groupname = $1 - )` - rows, err := db.QueryContext(ctx, query, groupname) + db, err := da.connect(ctx, DatabaseGort) if err != nil { - return users, gerr.Wrap(errs.ErrDataAccess, err) + return err } + defer db.Close() - for rows.Next() { - user := rest.User{} - - err = rows.Scan(&user.Email, &user.FullName, &user.Username) - if err != nil { - return users, gerr.Wrap(errs.ErrNoSuchUser, err) - } + // There will be more eventually + query := `UPDATE groupname + SET groupname=$1 + WHERE groupname=$1;` - users = append(users, user) + _, err = db.ExecContext(ctx, query, group.Name) + if err != nil { + err = gerr.Wrap(errs.ErrDataAccess, err) } - return users, nil + return err } -// GroupRemoveUser removes a user from a group. -func (da PostgresDataAccess) GroupRemoveUser(ctx context.Context, groupname string, username string) error { +// GroupUserAdd adds a user to a group +func (da PostgresDataAccess) GroupUserAdd(ctx context.Context, groupname string, username string) error { tr := otel.GetTracerProvider().Tracer(telemetry.ServiceName) - ctx, sp := tr.Start(ctx, "postgres.GroupRemoveUser") + ctx, sp := tr.Start(ctx, "postgres.GroupUserAdd") defer sp.End() if groupname == "" { @@ -399,13 +420,25 @@ func (da PostgresDataAccess) GroupRemoveUser(ctx context.Context, groupname stri return errs.ErrNoSuchGroup } + if username == "" { + return errs.ErrEmptyUserName + } + + exists, err = da.UserExists(ctx, username) + if err != nil { + return err + } + if !exists { + return errs.ErrNoSuchUser + } + db, err := da.connect(ctx, DatabaseGort) if err != nil { return err } defer db.Close() - query := "DELETE FROM groupusers WHERE groupname=$1 AND username=$2;" + query := `INSERT INTO groupusers (groupname, username) VALUES ($1, $2);` _, err = db.ExecContext(ctx, query, groupname, username) if err != nil { err = gerr.Wrap(errs.ErrDataAccess, err) @@ -414,18 +447,22 @@ func (da PostgresDataAccess) GroupRemoveUser(ctx context.Context, groupname stri return err } -// GroupRoleDelete revokes a role from a group. -func (da PostgresDataAccess) GroupRoleDelete(ctx context.Context, groupname, rolename string) error { +// GroupUserDelete removes a user from a group. +func (da PostgresDataAccess) GroupUserDelete(ctx context.Context, groupname string, username string) error { tr := otel.GetTracerProvider().Tracer(telemetry.ServiceName) - ctx, sp := tr.Start(ctx, "postgres.GroupRoleDelete") + ctx, sp := tr.Start(ctx, "postgres.GroupUserDelete") defer sp.End() if groupname == "" { return errs.ErrEmptyGroupName } - if rolename == "" { - return errs.ErrEmptyRoleName + exists, err := da.GroupExists(ctx, groupname) + if err != nil { + return err + } + if !exists { + return errs.ErrNoSuchGroup } db, err := da.connect(ctx, DatabaseGort) @@ -434,61 +471,64 @@ func (da PostgresDataAccess) GroupRoleDelete(ctx context.Context, groupname, rol } defer db.Close() - query := `DELETE FROM group_roles - WHERE group_name=$1 AND role_name=$2;` - _, err = db.ExecContext(ctx, query, groupname, rolename) + query := "DELETE FROM groupusers WHERE groupname=$1 AND username=$2;" + _, err = db.ExecContext(ctx, query, groupname, username) if err != nil { - return gerr.Wrap(errs.ErrDataAccess, err) + err = gerr.Wrap(errs.ErrDataAccess, err) } return err } -// GroupUpdate is used to update an existing group. An error is returned if the -// groupname is empty or if the group doesn't exist. -// TODO Should we let this create groups that don't exist? -func (da PostgresDataAccess) GroupUpdate(ctx context.Context, group rest.Group) error { +// GroupUserList returns a list of all known users in a group. +func (da PostgresDataAccess) GroupUserList(ctx context.Context, groupname string) ([]rest.User, error) { tr := otel.GetTracerProvider().Tracer(telemetry.ServiceName) - ctx, sp := tr.Start(ctx, "postgres.GroupUpdate") + ctx, sp := tr.Start(ctx, "postgres.GroupUserList") defer sp.End() - if group.Name == "" { - return errs.ErrEmptyGroupName + users := make([]rest.User, 0) + + if groupname == "" { + return users, errs.ErrEmptyGroupName } - exists, err := da.UserExists(ctx, group.Name) + exists, err := da.GroupExists(ctx, groupname) if err != nil { - return err + return users, err } if !exists { - return errs.ErrNoSuchGroup + return users, errs.ErrNoSuchGroup } db, err := da.connect(ctx, DatabaseGort) if err != nil { - return err + return users, err } defer db.Close() - // There will be more eventually - query := `UPDATE groupname - SET groupname=$1 - WHERE groupname=$1;` + query := `SELECT email, full_name, username + FROM users + WHERE username IN ( + SELECT username + FROM groupusers + WHERE groupname = $1 + )` - _, err = db.ExecContext(ctx, query, group.Name) + rows, err := db.QueryContext(ctx, query, groupname) if err != nil { - err = gerr.Wrap(errs.ErrDataAccess, err) + return users, gerr.Wrap(errs.ErrDataAccess, err) } - return err -} + for rows.Next() { + user := rest.User{} -// GroupUserAdd comments TBD -func (da PostgresDataAccess) GroupUserAdd(ctx context.Context, group string, user string) error { - return errs.ErrNotImplemented -} + err = rows.Scan(&user.Email, &user.FullName, &user.Username) + if err != nil { + return users, gerr.Wrap(errs.ErrNoSuchUser, err) + } -// GroupUserDelete comments TBD -func (da PostgresDataAccess) GroupUserDelete(ctx context.Context, group string, user string) error { - return errs.ErrNotImplemented + users = append(users, user) + } + + return users, nil } diff --git a/dataaccess/postgres/group-access_test.go b/dataaccess/postgres/group-access_test.go index 5513bdb..1ff6fe4 100644 --- a/dataaccess/postgres/group-access_test.go +++ b/dataaccess/postgres/group-access_test.go @@ -25,44 +25,85 @@ import ( ) func testGroupAccess(t *testing.T) { - t.Run("testGroupAddUser", testGroupAddUser) + t.Run("testGroupUserAdd", testGroupUserAdd) + t.Run("testGroupUserList", testGroupUserList) t.Run("testGroupCreate", testGroupCreate) t.Run("testGroupDelete", testGroupDelete) t.Run("testGroupExists", testGroupExists) t.Run("testGroupGet", testGroupGet) t.Run("testGroupRoleAdd", testGroupRoleAdd) + t.Run("testGroupPermissionList", testGroupPermissionList) t.Run("testGroupList", testGroupList) - t.Run("testGroupListRoles", testGroupListRoles) - t.Run("testGroupRemoveUser", testGroupRemoveUser) + t.Run("testGroupRoleList", testGroupRoleList) + t.Run("testGroupUserDelete", testGroupUserDelete) } -func testGroupAddUser(t *testing.T) { - err := da.GroupAddUser(ctx, "foo", "bar") +func testGroupUserAdd(t *testing.T) { + var ( + groupname = "group-test-group-user-add" + username = "user-test-group-user-add" + useremail = "user@foo.bar" + ) + + err := da.GroupUserAdd(ctx, groupname, username) assert.Error(t, err, errs.ErrNoSuchGroup) - da.GroupCreate(ctx, rest.Group{Name: "foo"}) - defer da.GroupDelete(ctx, "foo") + da.GroupCreate(ctx, rest.Group{Name: groupname}) + defer da.GroupDelete(ctx, groupname) - err = da.GroupAddUser(ctx, "foo", "bar") + err = da.GroupUserAdd(ctx, groupname, username) assert.Error(t, err, errs.ErrNoSuchUser) - da.UserCreate(ctx, rest.User{Username: "bar", Email: "bar"}) - defer da.UserDelete(ctx, "bar") + da.UserCreate(ctx, rest.User{Username: username, Email: useremail}) + defer da.UserDelete(ctx, username) - err = da.GroupAddUser(ctx, "foo", "bar") + err = da.GroupUserAdd(ctx, groupname, username) assert.NoError(t, err) - group, _ := da.GroupGet(ctx, "foo") + group, _ := da.GroupGet(ctx, groupname) - if len(group.Users) != 1 { - t.Error("Users list empty") + if !assert.Len(t, group.Users, 1) { t.FailNow() } - if len(group.Users) > 0 && group.Users[0].Username != "bar" { - t.Error("Wrong user!") + assert.Equal(t, group.Users[0].Username, username) + assert.Equal(t, group.Users[0].Email, useremail) +} + +func testGroupUserList(t *testing.T) { + var ( + groupname = "group-test-group-user-list" + expected = []rest.User{ + {Username: "user-test-group-user-list-0", Email: "user-test-group-user-list-0@email.com"}, + {Username: "user-test-group-user-list-1", Email: "user-test-group-user-list-1@email.com"}, + } + ) + + _, err := da.GroupUserList(ctx, groupname) + assert.Error(t, err) + assert.ErrorIs(t, err, errs.ErrNoSuchGroup) + + da.GroupCreate(ctx, rest.Group{Name: groupname}) + defer da.GroupDelete(ctx, groupname) + + da.UserCreate(ctx, expected[0]) + defer da.UserDelete(ctx, expected[0].Username) + da.UserCreate(ctx, expected[1]) + defer da.UserDelete(ctx, expected[1].Username) + + da.GroupUserAdd(ctx, groupname, expected[0].Username) + da.GroupUserAdd(ctx, groupname, expected[1].Username) + + actual, err := da.GroupUserList(ctx, groupname) + if !assert.NoError(t, err) { + t.FailNow() + } + + if !assert.Len(t, actual, 2) { t.FailNow() } + + assert.Equal(t, expected, actual) } func testGroupCreate(t *testing.T) { @@ -126,34 +167,74 @@ func testGroupExists(t *testing.T) { } func testGroupGet(t *testing.T) { + groupname := "group-test-group-get" + var err error var group rest.Group // Expect an error _, err = da.GroupGet(ctx, "") - assert.Error(t, err, errs.ErrEmptyGroupName) + assert.ErrorIs(t, err, errs.ErrEmptyGroupName) // Expect an error - _, err = da.GroupGet(ctx, "test-get") - assert.Error(t, err, errs.ErrNoSuchGroup) + _, err = da.GroupGet(ctx, groupname) + assert.ErrorIs(t, err, errs.ErrNoSuchGroup) - da.GroupCreate(ctx, rest.Group{Name: "test-get"}) - defer da.GroupDelete(ctx, "test-get") + da.GroupCreate(ctx, rest.Group{Name: groupname}) + defer da.GroupDelete(ctx, groupname) // da.Group ctx, should exist now - exists, _ := da.GroupExists(ctx, "test-get") - if !exists { - t.Error("Group should exist now") + exists, _ := da.GroupExists(ctx, groupname) + if !assert.True(t, exists) { t.FailNow() } // Expect no error - group, err = da.GroupGet(ctx, "test-get") - assert.NoError(t, err) - if group.Name != "test-get" { - t.Errorf("Group name mismatch: %q is not \"test-get\"", group.Name) + group, err = da.GroupGet(ctx, groupname) + if !assert.NoError(t, err) { + t.FailNow() + } + if !assert.Equal(t, groupname, group.Name) { + t.FailNow() + } +} + +func testGroupPermissionList(t *testing.T) { + const ( + groupname = "group-test-group-permission-list" + rolename = "role-test-group-permission-list" + bundlename = "test" + ) + + var expected = rest.RolePermissionList{ + {BundleName: bundlename, Permission: "role-test-group-permission-list-1"}, + {BundleName: bundlename, Permission: "role-test-group-permission-list-2"}, + {BundleName: bundlename, Permission: "role-test-group-permission-list-3"}, + } + + var err error + + da.GroupCreate(ctx, rest.Group{Name: groupname}) + defer da.GroupDelete(ctx, groupname) + + da.RoleCreate(ctx, rolename) + defer da.RoleDelete(ctx, rolename) + + err = da.GroupRoleAdd(ctx, groupname, rolename) + if !assert.NoError(t, err) { + t.FailNow() + } + + da.RolePermissionAdd(ctx, rolename, expected[0].BundleName, expected[0].Permission) + da.RolePermissionAdd(ctx, rolename, expected[1].BundleName, expected[1].Permission) + da.RolePermissionAdd(ctx, rolename, expected[2].BundleName, expected[2].Permission) + + actual, err := da.GroupPermissionList(ctx, groupname) + if !assert.NoError(t, err) { t.FailNow() } + + assert.Equal(t, expected, actual) } func testGroupRoleAdd(t *testing.T) { @@ -190,7 +271,7 @@ func testGroupRoleAdd(t *testing.T) { }, } - roles, err := da.GroupListRoles(ctx, groupName) + roles, err := da.GroupRoleList(ctx, groupName) if !assert.NoError(t, err) { t.FailNow() } @@ -204,7 +285,7 @@ func testGroupRoleAdd(t *testing.T) { expectedRoles = []rest.Role{} - roles, err = da.GroupListRoles(ctx, groupName) + roles, err = da.GroupRoleList(ctx, groupName) if !assert.NoError(t, err) { t.FailNow() } @@ -238,45 +319,53 @@ func testGroupList(t *testing.T) { } } -func testGroupListRoles(t *testing.T) { - da.GroupCreate(ctx, rest.Group{Name: "group-test-group-list-roles"}) - defer da.GroupDelete(ctx, "group-test-group-list-roles") +func testGroupRoleList(t *testing.T) { + var ( + groupname = "group-test-group-list-roles" + rolenames = []string{ + "role-test-group-list-roles-0", + "role-test-group-list-roles-1", + "role-test-group-list-roles-2", + } + ) + da.GroupCreate(ctx, rest.Group{Name: groupname}) + defer da.GroupDelete(ctx, groupname) - da.RoleCreate(ctx, "role-test-group-list-roles-1") - defer da.RoleDelete(ctx, "role-test-group-list-roles-1") + da.RoleCreate(ctx, rolenames[1]) + defer da.RoleDelete(ctx, rolenames[1]) - da.RoleCreate(ctx, "role-test-group-list-roles-0") - defer da.RoleDelete(ctx, "role-test-group-list-roles-0") + da.RoleCreate(ctx, rolenames[0]) + defer da.RoleDelete(ctx, rolenames[0]) - da.RoleCreate(ctx, "role-test-group-list-roles-2") - defer da.RoleDelete(ctx, "role-test-group-list-roles-2") + da.RoleCreate(ctx, rolenames[2]) + defer da.RoleDelete(ctx, rolenames[2]) - roles, err := da.GroupListRoles(ctx, "group-test-group-list-roles") + roles, err := da.GroupRoleList(ctx, groupname) if !assert.NoError(t, err) && !assert.Empty(t, roles) { t.FailNow() } - err = da.GroupRoleAdd(ctx, "group-test-group-list-roles", "role-test-group-list-roles-1") + err = da.GroupRoleAdd(ctx, groupname, rolenames[1]) if !assert.NoError(t, err) { t.FailNow() } - err = da.GroupRoleAdd(ctx, "group-test-group-list-roles", "role-test-group-list-roles-0") + err = da.GroupRoleAdd(ctx, groupname, rolenames[0]) if !assert.NoError(t, err) { t.FailNow() } - err = da.GroupRoleAdd(ctx, "group-test-group-list-roles", "role-test-group-list-roles-2") + err = da.GroupRoleAdd(ctx, groupname, rolenames[2]) if !assert.NoError(t, err) { t.FailNow() } // Note: alphabetically sorted! expected := []rest.Role{ - {Name: "role-test-group-list-roles-0", Permissions: []rest.RolePermission{}}, - {Name: "role-test-group-list-roles-1", Permissions: []rest.RolePermission{}}, - {Name: "role-test-group-list-roles-2", Permissions: []rest.RolePermission{}}, + {Name: rolenames[0], Permissions: []rest.RolePermission{}}, + {Name: rolenames[1], Permissions: []rest.RolePermission{}}, + {Name: rolenames[2], Permissions: []rest.RolePermission{}}, } - actual, err := da.GroupListRoles(ctx, "group-test-group-list-roles") + actual, err := da.GroupRoleList(ctx, groupname) if !assert.NoError(t, err) { t.FailNow() } @@ -284,14 +373,14 @@ func testGroupListRoles(t *testing.T) { assert.Equal(t, expected, actual) } -func testGroupRemoveUser(t *testing.T) { +func testGroupUserDelete(t *testing.T) { da.GroupCreate(ctx, rest.Group{Name: "foo"}) defer da.GroupDelete(ctx, "foo") da.UserCreate(ctx, rest.User{Username: "bat"}) defer da.UserDelete(ctx, "bat") - err := da.GroupAddUser(ctx, "foo", "bat") + err := da.GroupUserAdd(ctx, "foo", "bat") assert.NoError(t, err) group, err := da.GroupGet(ctx, "foo") @@ -307,7 +396,7 @@ func testGroupRemoveUser(t *testing.T) { t.FailNow() } - err = da.GroupRemoveUser(ctx, "foo", "bat") + err = da.GroupUserDelete(ctx, "foo", "bat") assert.NoError(t, err) group, err = da.GroupGet(ctx, "foo") diff --git a/dataaccess/postgres/role-access.go b/dataaccess/postgres/role-access.go index 03d6ca7..ba2b037 100644 --- a/dataaccess/postgres/role-access.go +++ b/dataaccess/postgres/role-access.go @@ -62,65 +62,6 @@ func (da PostgresDataAccess) RoleCreate(ctx context.Context, name string) error return err } -// RoleList gets all roles. -func (da PostgresDataAccess) RoleList(ctx context.Context) ([]rest.Role, error) { - tr := otel.GetTracerProvider().Tracer(telemetry.ServiceName) - ctx, sp := tr.Start(ctx, "postgres.RoleList") - defer sp.End() - - db, err := da.connect(ctx, DatabaseGort) - if err != nil { - return nil, err - } - defer db.Close() - - var rolesByName = make(map[string]*rest.Role) - // Load all role names and add to the roles map - query := `SELECT role_name - FROM roles` - - rows, err := db.QueryContext(ctx, query) - if err != nil { - return nil, gerr.Wrap(errs.ErrNoSuchRole, err) - } - defer rows.Close() - - for rows.Next() { - var name string - if err := rows.Scan(&name); err != nil { - log.Fatal(err) - } - rolesByName[name] = &rest.Role{Name: name} - } - - // Load all permissions and add to role objects - query = `SELECT role_name, bundle_name, permission - FROM role_permissions` - rows, err = db.QueryContext(ctx, query) - if err != nil { - return nil, gerr.Wrap(errs.ErrNoSuchRole, err) - } - defer rows.Close() - - for rows.Next() { - var ( - rolename string - permission rest.RolePermission - ) - if err := rows.Scan(&rolename, &permission.BundleName, &permission.Permission); err != nil { - log.Fatal(err) - } - rolesByName[rolename].Permissions = append(rolesByName[rolename].Permissions, permission) - } - - var roles []rest.Role - for _, role := range rolesByName { - roles = append(roles, *role) - } - - return roles, nil -} - // RoleDelete func (da PostgresDataAccess) RoleDelete(ctx context.Context, name string) error { tr := otel.GetTracerProvider().Tracer(telemetry.ServiceName) @@ -225,6 +166,160 @@ func (da PostgresDataAccess) RoleGet(ctx context.Context, name string) (rest.Rol return role, nil } +func (da PostgresDataAccess) RoleGroupAdd(ctx context.Context, rolename, groupname string) error { + tr := otel.GetTracerProvider().Tracer(telemetry.ServiceName) + ctx, sp := tr.Start(ctx, "postgres.RoleGroupAdd") + defer sp.End() + + return da.GroupRoleAdd(ctx, groupname, rolename) +} + +func (da PostgresDataAccess) RoleGroupDelete(ctx context.Context, rolename, groupname string) error { + tr := otel.GetTracerProvider().Tracer(telemetry.ServiceName) + ctx, sp := tr.Start(ctx, "postgres.RoleGroupDelete") + defer sp.End() + + return da.GroupRoleDelete(ctx, groupname, rolename) +} + +func (da PostgresDataAccess) RoleGroupExists(ctx context.Context, rolename, groupname string) (bool, error) { + tr := otel.GetTracerProvider().Tracer(telemetry.ServiceName) + ctx, sp := tr.Start(ctx, "postgres.RoleGroupExists") + defer sp.End() + + groups, err := da.RoleGroupList(ctx, rolename) + if err != nil { + return false, err + } + + if exists, err := da.GroupExists(ctx, groupname); err != nil { + return false, err + } else if !exists { + return false, errs.ErrNoSuchGroup + } + + for _, g := range groups { + if g.Name == groupname { + return true, nil + } + } + + return false, nil +} + +func (da PostgresDataAccess) RoleGroupList(ctx context.Context, rolename string) ([]rest.Group, error) { + tr := otel.GetTracerProvider().Tracer(telemetry.ServiceName) + ctx, sp := tr.Start(ctx, "postgres.RoleGroupList") + defer sp.End() + + if rolename == "" { + return nil, errs.ErrEmptyRoleName + } + + exists, err := da.RoleExists(ctx, rolename) + if err != nil { + return nil, err + } + if !exists { + return nil, errs.ErrNoSuchRole + } + + db, err := da.connect(ctx, DatabaseGort) + if err != nil { + return nil, err + } + defer db.Close() + + query := `SELECT group_name + FROM group_roles + WHERE role_name = $1 + ORDER BY role_name` + + rows, err := db.QueryContext(ctx, query, rolename) + if err != nil { + return nil, gerr.Wrap(errs.ErrDataAccess, err) + } + + groups := []rest.Group{} + + for rows.Next() { + var name string + + err = rows.Scan(&name) + if err != nil { + return nil, gerr.Wrap(errs.ErrDataAccess, err) + } + + group, err := da.GroupGet(ctx, name) + if err != nil { + return nil, err + } + + groups = append(groups, group) + } + + return groups, nil +} + +// RoleList gets all roles. +func (da PostgresDataAccess) RoleList(ctx context.Context) ([]rest.Role, error) { + tr := otel.GetTracerProvider().Tracer(telemetry.ServiceName) + ctx, sp := tr.Start(ctx, "postgres.RoleList") + defer sp.End() + + db, err := da.connect(ctx, DatabaseGort) + if err != nil { + return nil, err + } + defer db.Close() + + var rolesByName = make(map[string]*rest.Role) + // Load all role names and add to the roles map + query := `SELECT role_name + FROM roles` + + rows, err := db.QueryContext(ctx, query) + if err != nil { + return nil, gerr.Wrap(errs.ErrNoSuchRole, err) + } + defer rows.Close() + + for rows.Next() { + var name string + if err := rows.Scan(&name); err != nil { + log.Fatal(err) + } + rolesByName[name] = &rest.Role{Name: name} + } + + // Load all permissions and add to role objects + query = `SELECT role_name, bundle_name, permission + FROM role_permissions` + rows, err = db.QueryContext(ctx, query) + if err != nil { + return nil, gerr.Wrap(errs.ErrNoSuchRole, err) + } + defer rows.Close() + + for rows.Next() { + var ( + rolename string + permission rest.RolePermission + ) + if err := rows.Scan(&rolename, &permission.BundleName, &permission.Permission); err != nil { + log.Fatal(err) + } + rolesByName[rolename].Permissions = append(rolesByName[rolename].Permissions, permission) + } + + var roles []rest.Role + for _, role := range rolesByName { + roles = append(roles, *role) + } + + return roles, nil +} + func (da PostgresDataAccess) RolePermissionAdd(ctx context.Context, rolename, bundle, permission string) error { tr := otel.GetTracerProvider().Tracer(telemetry.ServiceName) ctx, sp := tr.Start(ctx, "postgres.RolePermissionAdd") @@ -299,46 +394,10 @@ func (da PostgresDataAccess) RolePermissionDelete(ctx context.Context, rolename, return err } -func (da PostgresDataAccess) doGetRolePermissions(ctx context.Context, name string) ([]rest.RolePermission, error) { - tr := otel.GetTracerProvider().Tracer(telemetry.ServiceName) - ctx, sp := tr.Start(ctx, "postgres.doGetRolePermissions") - defer sp.End() - - perms := make([]rest.RolePermission, 0) - - db, err := da.connect(ctx, DatabaseGort) - if err != nil { - return perms, err - } - defer db.Close() - - query := `SELECT bundle_name, permission - FROM role_permissions - WHERE role_name = $1` - - rows, err := db.QueryContext(ctx, query, name) - if err != nil { - return perms, gerr.Wrap(errs.ErrDataAccess, err) - } - - for rows.Next() { - perm := rest.RolePermission{} - - err = rows.Scan(&perm.BundleName, &perm.Permission) - if err != nil { - return perms, gerr.Wrap(errs.ErrNoSuchUser, err) - } - - perms = append(perms, perm) - } - - return perms, nil -} - -// RoleHasPermission returns true if the given role has been granted the +// RolePermissionExists returns true if the given role has been granted the // specified permission. It returns an error if rolename is empty or if no // such role exists. -func (da PostgresDataAccess) RoleHasPermission(ctx context.Context, rolename, bundlename, permission string) (bool, error) { +func (da PostgresDataAccess) RolePermissionExists(ctx context.Context, rolename, bundlename, permission string) (bool, error) { // TODO Make this more efficient. perms, err := da.RolePermissionList(ctx, rolename) @@ -358,7 +417,7 @@ func (da PostgresDataAccess) RoleHasPermission(ctx context.Context, rolename, bu // RolePermissionList returns returns an alphabetically-sorted list of // fully-qualified (i.e., "bundle:permission") permissions granted to // the role. -func (da PostgresDataAccess) RolePermissionList(ctx context.Context, rolename string) ([]rest.RolePermission, error) { +func (da PostgresDataAccess) RolePermissionList(ctx context.Context, rolename string) (rest.RolePermissionList, error) { // TODO Make this more efficient. role, err := da.RoleGet(ctx, rolename) @@ -372,3 +431,39 @@ func (da PostgresDataAccess) RolePermissionList(ctx context.Context, rolename st return perms, nil } + +func (da PostgresDataAccess) doGetRolePermissions(ctx context.Context, name string) (rest.RolePermissionList, error) { + tr := otel.GetTracerProvider().Tracer(telemetry.ServiceName) + ctx, sp := tr.Start(ctx, "postgres.doGetRolePermissions") + defer sp.End() + + perms := make([]rest.RolePermission, 0) + + db, err := da.connect(ctx, DatabaseGort) + if err != nil { + return perms, err + } + defer db.Close() + + query := `SELECT bundle_name, permission + FROM role_permissions + WHERE role_name = $1` + + rows, err := db.QueryContext(ctx, query, name) + if err != nil { + return perms, gerr.Wrap(errs.ErrDataAccess, err) + } + + for rows.Next() { + perm := rest.RolePermission{} + + err = rows.Scan(&perm.BundleName, &perm.Permission) + if err != nil { + return perms, gerr.Wrap(errs.ErrNoSuchUser, err) + } + + perms = append(perms, perm) + } + + return perms, nil +} diff --git a/dataaccess/postgres/role-access_test.go b/dataaccess/postgres/role-access_test.go index 4dd2673..3297bba 100644 --- a/dataaccess/postgres/role-access_test.go +++ b/dataaccess/postgres/role-access_test.go @@ -30,7 +30,12 @@ func testRoleAccess(t *testing.T) { t.Run("testRoleExists", testRoleExists) t.Run("testRoleDelete", testRoleDelete) t.Run("testRoleGet", testRoleGet) - t.Run("testRoleHasPermission", testRoleHasPermission) + t.Run("testRoleGroupAdd", testRoleGroupAdd) + t.Run("testRoleGroupDelete", testRoleGroupDelete) + t.Run("testRoleGroupExists", testRoleGroupExists) + t.Run("testRoleGroupList", testRoleGroupList) + t.Run("testRolePermissionExists", testRolePermissionExists) + t.Run("testRolePermissionAdd", testRolePermissionAdd) t.Run("testRolePermissionList", testRolePermissionList) } @@ -164,13 +169,193 @@ func testRoleGet(t *testing.T) { assert.Equal(t, expected, role) } -func testRoleHasPermission(t *testing.T) { +func testRoleGroupAdd(t *testing.T) { + var err error + + rolename := "role-test-role-group-add" + groupnames := []string{ + "perm-test-role-group-add-0", + "perm-test-role-group-add-1", + } + + // No such group yet + err = da.RoleGroupAdd(ctx, rolename, groupnames[1]) + assert.ErrorIs(t, err, errs.ErrNoSuchGroup) + + da.GroupCreate(ctx, rest.Group{Name: groupnames[0]}) + defer da.GroupDelete(ctx, groupnames[0]) + da.GroupCreate(ctx, rest.Group{Name: groupnames[1]}) + defer da.GroupDelete(ctx, groupnames[1]) + + // Groups exist now, but the role doesn't + err = da.RoleGroupAdd(ctx, rolename, groupnames[1]) + assert.ErrorIs(t, err, errs.ErrNoSuchRole) + + da.RoleCreate(ctx, rolename) + defer da.RoleDelete(ctx, rolename) + + for _, groupname := range groupnames { + err = da.RoleGroupAdd(ctx, rolename, groupname) + assert.NoError(t, err) + } + + for _, groupname := range groupnames { + exists, _ := da.RoleGroupExists(ctx, rolename, groupname) + assert.True(t, exists, groupname) + } +} + +func testRoleGroupDelete(t *testing.T) { + +} + +func testRoleGroupExists(t *testing.T) { + var err error + + rolename := "role-test-role-group-exists" + groupnames := []string{ + "group-test-role-group-exists-0", + "group-test-role-group-exists-1", + } + groupnull := "group-test-role-group-exists-null" + + // No such role yet + _, err = da.RoleGroupExists(ctx, rolename, groupnames[1]) + assert.ErrorIs(t, err, errs.ErrNoSuchRole) + + da.RoleCreate(ctx, rolename) + defer da.RoleDelete(ctx, rolename) + + // Groups exist now, but the role doesn't + _, err = da.RoleGroupExists(ctx, rolename, groupnames[1]) + assert.ErrorIs(t, err, errs.ErrNoSuchGroup) + + da.GroupCreate(ctx, rest.Group{Name: groupnames[0]}) + defer da.GroupDelete(ctx, groupnames[0]) + da.GroupCreate(ctx, rest.Group{Name: groupnames[1]}) + defer da.GroupDelete(ctx, groupnames[1]) + da.GroupCreate(ctx, rest.Group{Name: groupnull}) + defer da.GroupDelete(ctx, groupnull) + + for _, groupname := range groupnames { + da.RoleGroupAdd(ctx, rolename, groupname) + } + + for _, groupname := range groupnames { + exists, err := da.RoleGroupExists(ctx, rolename, groupname) + assert.NoError(t, err) + assert.True(t, exists) + } + + // Null group should NOT exist on the role + exists, err := da.RoleGroupExists(ctx, rolename, groupnull) + assert.NoError(t, err) + assert.False(t, exists) +} + +func testRoleGroupList(t *testing.T) { + var err error + + rolename := "role-test-role-group-list" + groupnames := []string{ + "group-test-role-group-list-0", + "group-test-role-group-list-1", + } + groupnull := "group-test-role-group-list-null" + + // No such role yet + _, err = da.RoleGroupList(ctx, rolename) + assert.ErrorIs(t, err, errs.ErrNoSuchRole) + + da.RoleCreate(ctx, rolename) + defer da.RoleDelete(ctx, rolename) + + // Groups exist now, but the role doesn't + groups, err := da.RoleGroupList(ctx, rolename) + assert.NoError(t, err) + assert.Empty(t, groups) + + da.GroupCreate(ctx, rest.Group{Name: groupnames[1]}) + defer da.GroupDelete(ctx, groupnames[1]) + da.GroupCreate(ctx, rest.Group{Name: groupnames[0]}) + defer da.GroupDelete(ctx, groupnames[0]) + da.GroupCreate(ctx, rest.Group{Name: groupnull}) + defer da.GroupDelete(ctx, groupnull) + + for _, groupname := range groupnames { + da.RoleGroupAdd(ctx, rolename, groupname) + } + + // Currently the groups are NOT expected to be fully described (i.e., + // their roles slices don't have to be complete). + groups, err = da.RoleGroupList(ctx, rolename) + assert.NoError(t, err) + assert.Len(t, groups, 2) + + for i, g := range groups { + assert.Equal(t, groupnames[i], g.Name) + } +} + +func testRolePermissionAdd(t *testing.T) { + var exists bool + var err error + + const rolename = "role-test-role-permission-add" + const bundlename = "test" + const permname1 = "perm-test-role-permission-add-0" + const permname2 = "perm-test-role-permission-add-1" + + da.RoleCreate(ctx, rolename) + defer da.RoleDelete(ctx, rolename) + + role, _ := da.RoleGet(ctx, rolename) + if !assert.Len(t, role.Permissions, 0) { + t.FailNow() + } + + // First permission + err = da.RolePermissionAdd(ctx, rolename, bundlename, permname1) + if !assert.NoError(t, err) { + t.FailNow() + } + defer da.RolePermissionDelete(ctx, rolename, bundlename, permname1) + + role, _ = da.RoleGet(ctx, rolename) + if !assert.Len(t, role.Permissions, 1) { + t.FailNow() + } + + exists, _ = da.RolePermissionExists(ctx, rolename, bundlename, permname1) + if !assert.True(t, exists) { + t.FailNow() + } + + // Second permission + err = da.RolePermissionAdd(ctx, rolename, bundlename, permname2) + if !assert.NoError(t, err) { + t.FailNow() + } + defer da.RolePermissionDelete(ctx, rolename, bundlename, permname2) + + role, _ = da.RoleGet(ctx, rolename) + if !assert.Len(t, role.Permissions, 2) { + t.FailNow() + } + + exists, _ = da.RolePermissionExists(ctx, rolename, bundlename, permname2) + if !assert.True(t, exists) { + t.FailNow() + } +} + +func testRolePermissionExists(t *testing.T) { var err error da.RoleCreate(ctx, "role-test-role-has-permission") defer da.RoleDelete(ctx, "role-test-role-has-permission") - has, err := da.RoleHasPermission(ctx, "role-test-role-has-permission", "test", "permission-test-role-has-permission-1") + has, err := da.RolePermissionExists(ctx, "role-test-role-has-permission", "test", "permission-test-role-has-permission-1") if !assert.NoError(t, err) || !assert.False(t, has) { t.FailNow() } @@ -181,12 +366,12 @@ func testRoleHasPermission(t *testing.T) { } defer da.RolePermissionDelete(ctx, "role-test-role-has-permission", "test", "permission-test-role-has-permission-1") - has, err = da.RoleHasPermission(ctx, "role-test-role-has-permission", "test", "permission-test-role-has-permission-1") + has, err = da.RolePermissionExists(ctx, "role-test-role-has-permission", "test", "permission-test-role-has-permission-1") if !assert.NoError(t, err) || !assert.True(t, has) { t.FailNow() } - has, err = da.RoleHasPermission(ctx, "role-test-role-has-permission", "test", "permission-test-role-has-permission-2") + has, err = da.RolePermissionExists(ctx, "role-test-role-has-permission", "test", "permission-test-role-has-permission-2") if !assert.NoError(t, err) || !assert.False(t, has) { t.FailNow() } @@ -217,7 +402,7 @@ func testRolePermissionList(t *testing.T) { defer da.RolePermissionDelete(ctx, "role-test-role-permission-list", "test", "permission-test-role-permission-list-2") // Expect a sorted list! - expect := []rest.RolePermission{ + expect := rest.RolePermissionList{ {BundleName: "test", Permission: "permission-test-role-permission-list-1"}, {BundleName: "test", Permission: "permission-test-role-permission-list-2"}, {BundleName: "test", Permission: "permission-test-role-permission-list-3"}, diff --git a/dataaccess/postgres/user-access.go b/dataaccess/postgres/user-access.go index c5f3180..ba1e531 100644 --- a/dataaccess/postgres/user-access.go +++ b/dataaccess/postgres/user-access.go @@ -240,139 +240,6 @@ func (da PostgresDataAccess) UserGetByEmail(ctx context.Context, email string) ( return user, err } -// UserList returns a list of all known users in the datastore. -// Passwords are not included. Nice try. -func (da PostgresDataAccess) UserList(ctx context.Context) ([]rest.User, error) { - tr := otel.GetTracerProvider().Tracer(telemetry.ServiceName) - ctx, sp := tr.Start(ctx, "postgres.UserList") - defer sp.End() - - db, err := da.connect(ctx, DatabaseGort) - if err != nil { - return nil, err - } - defer db.Close() - - query := `SELECT email, full_name, username FROM users` - rows, err := db.QueryContext(ctx, query) - if err != nil { - return nil, err - } - defer rows.Close() - - users := make([]rest.User, 0) - for rows.Next() { - user := rest.User{} - err = rows.Scan(&user.Email, &user.FullName, &user.Username) - if err != nil { - err = gerr.Wrap(errs.ErrNoSuchUser, err) - } - users = append(users, user) - } - - return users, err -} - -// UserPermissions returns an alphabetically-sorted list of fully-qualified -// (i.e., "bundle:permission") permissions available to the specified user. -func (da PostgresDataAccess) UserPermissions(ctx context.Context, username string) ([]string, error) { - // TODO This is horribly inefficient -- use a real SQL query instead! - - tr := otel.GetTracerProvider().Tracer(telemetry.ServiceName) - ctx, sp := tr.Start(ctx, "postgres.UserPermissions") - defer sp.End() - - pp := []string{} - - groups, err := da.UserGroupList(ctx, username) - if err != nil { - return nil, err - } - - for _, group := range groups { - roles, err := da.GroupListRoles(ctx, group.Name) - if err != nil { - return nil, err - } - - for _, role := range roles { - for _, p := range role.Permissions { - pp = append(pp, p.BundleName+":"+p.Permission) - } - } - } - - sort.Strings(pp) - - return pp, nil -} - -// UserUpdate is used to update an existing user. An error is returned if the -// username is empty or if the user doesn't exist. -func (da PostgresDataAccess) UserUpdate(ctx context.Context, user rest.User) error { - tr := otel.GetTracerProvider().Tracer(telemetry.ServiceName) - ctx, sp := tr.Start(ctx, "postgres.UserUpdate") - defer sp.End() - - if user.Username == "" { - return errs.ErrEmptyUserName - } - - exists, err := da.UserExists(ctx, user.Username) - if err != nil { - return err - } - if !exists { - return errs.ErrNoSuchUser - } - - db, err := da.connect(ctx, DatabaseGort) - if err != nil { - return err - } - defer db.Close() - - query := `SELECT email, full_name, username, password_hash - FROM users - WHERE username=$1` - - userOld := rest.User{} - err = db. - QueryRowContext(ctx, query, user.Username). - Scan(&userOld.Email, &userOld.FullName, &userOld.Username, &userOld.Password) - - if err != nil { - return gerr.Wrap(errs.ErrNoSuchUser, err) - } - - if user.Email != "" { - userOld.Email = user.Email - } - - if user.FullName != "" { - userOld.FullName = user.FullName - } - - if user.Password != "" { - userOld.Password, err = data.HashPassword(user.Password) - if err != nil { - return err - } - } - - query = `UPDATE users - SET email=$1, full_name=$2, password_hash=$3 - WHERE username=$4;` - - _, err = db.ExecContext(ctx, query, userOld.Email, userOld.FullName, userOld.Password, userOld.Username) - - if err != nil { - err = gerr.Wrap(errs.ErrDataAccess, err) - } - - return err -} - // UserGroupList returns a slice of Group values representing the specified user's group memberships. // The groups' Users slice is never populated, and is always nil. func (da PostgresDataAccess) UserGroupList(ctx context.Context, username string) ([]rest.Group, error) { @@ -501,3 +368,174 @@ func (da PostgresDataAccess) UserGroupDelete(ctx context.Context, username strin return err } + +// UserList returns a list of all known users in the datastore. +// Passwords are not included. Nice try. +func (da PostgresDataAccess) UserList(ctx context.Context) ([]rest.User, error) { + tr := otel.GetTracerProvider().Tracer(telemetry.ServiceName) + ctx, sp := tr.Start(ctx, "postgres.UserList") + defer sp.End() + + db, err := da.connect(ctx, DatabaseGort) + if err != nil { + return nil, err + } + defer db.Close() + + query := `SELECT email, full_name, username FROM users` + rows, err := db.QueryContext(ctx, query) + if err != nil { + return nil, err + } + defer rows.Close() + + users := make([]rest.User, 0) + for rows.Next() { + user := rest.User{} + err = rows.Scan(&user.Email, &user.FullName, &user.Username) + if err != nil { + err = gerr.Wrap(errs.ErrNoSuchUser, err) + } + users = append(users, user) + } + + return users, err +} + +// UserPermissionList returns an alphabetically-sorted list of permissions +// available to the specified user. +func (da PostgresDataAccess) UserPermissionList(ctx context.Context, username string) (rest.RolePermissionList, error) { + // TODO This is HORRIBLY inefficient -- use a real SQL query instead! + + tr := otel.GetTracerProvider().Tracer(telemetry.ServiceName) + ctx, sp := tr.Start(ctx, "postgres.UserPermissionList") + defer sp.End() + + pp := []rest.RolePermission{} + + groups, err := da.UserGroupList(ctx, username) + if err != nil { + return nil, err + } + + for _, group := range groups { + roles, err := da.GroupRoleList(ctx, group.Name) + if err != nil { + return nil, err + } + + for _, role := range roles { + rp, err := da.RolePermissionList(ctx, role.Name) + if err != nil { + return nil, err + } + + for _, p := range rp { + pp = append(pp, p) + } + } + } + + sort.Slice(pp, func(i, j int) bool { return pp[i].String() < pp[j].String() }) + + return pp, nil +} + +// UserRoleList returns a slice of Role values representing the specified +// user's indirect roles (indirect because users are members of groups, +// and groups have roles). +func (da PostgresDataAccess) UserRoleList(ctx context.Context, username string) ([]rest.Role, error) { + rm := map[string]rest.Role{} + + groups, err := da.UserGroupList(ctx, username) + if err != nil { + return []rest.Role{}, err + } + + for _, gr := range groups { + rl, err := da.GroupRoleList(ctx, gr.Name) + if err != nil { + return []rest.Role{}, err + } + + for _, r := range rl { + rm[r.Name] = r + } + } + + roles := []rest.Role{} + + for _, r := range rm { + roles = append(roles, r) + } + + sort.Slice(roles, func(i, j int) bool { return roles[i].Name < roles[j].Name }) + + return roles, nil +} + +// UserUpdate is used to update an existing user. An error is returned if the +// username is empty or if the user doesn't exist. +func (da PostgresDataAccess) UserUpdate(ctx context.Context, user rest.User) error { + tr := otel.GetTracerProvider().Tracer(telemetry.ServiceName) + ctx, sp := tr.Start(ctx, "postgres.UserUpdate") + defer sp.End() + + if user.Username == "" { + return errs.ErrEmptyUserName + } + + exists, err := da.UserExists(ctx, user.Username) + if err != nil { + return err + } + if !exists { + return errs.ErrNoSuchUser + } + + db, err := da.connect(ctx, DatabaseGort) + if err != nil { + return err + } + defer db.Close() + + query := `SELECT email, full_name, username, password_hash + FROM users + WHERE username=$1` + + userOld := rest.User{} + err = db. + QueryRowContext(ctx, query, user.Username). + Scan(&userOld.Email, &userOld.FullName, &userOld.Username, &userOld.Password) + + if err != nil { + return gerr.Wrap(errs.ErrNoSuchUser, err) + } + + if user.Email != "" { + userOld.Email = user.Email + } + + if user.FullName != "" { + userOld.FullName = user.FullName + } + + if user.Password != "" { + userOld.Password, err = data.HashPassword(user.Password) + if err != nil { + return err + } + } + + query = `UPDATE users + SET email=$1, full_name=$2, password_hash=$3 + WHERE username=$4;` + + _, err = db.ExecContext(ctx, query, userOld.Email, userOld.FullName, userOld.Password, userOld.Username) + + if err != nil { + err = gerr.Wrap(errs.ErrDataAccess, err) + } + + return err +} diff --git a/dataaccess/postgres/user-access_test.go b/dataaccess/postgres/user-access_test.go index b99960e..4e2cabc 100644 --- a/dataaccess/postgres/user-access_test.go +++ b/dataaccess/postgres/user-access_test.go @@ -33,7 +33,7 @@ func testUserAccess(t *testing.T) { t.Run("testUserGroupList", testUserGroupList) t.Run("testUserList", testUserList) t.Run("testUserNotExists", testUserNotExists) - t.Run("testUserPermissions", testUserPermissions) + t.Run("testUserPermissionList", testUserPermissionList) t.Run("testUserUpdate", testUserUpdate) } @@ -179,7 +179,7 @@ func testUserGroupList(t *testing.T) { da.UserCreate(ctx, rest.User{Username: "user-test-user-group-list"}) defer da.UserDelete(ctx, "user-test-user-group-list") - da.GroupAddUser(ctx, "group-test-user-group-list-0", "user-test-user-group-list") + da.GroupUserAdd(ctx, "group-test-user-group-list-0", "user-test-user-group-list") expected := []rest.Group{{Name: "group-test-user-group-list-0", Users: nil}} @@ -238,7 +238,8 @@ func testUserNotExists(t *testing.T) { t.FailNow() } } -func testUserPermissions(t *testing.T) { + +func testUserPermissionList(t *testing.T) { var err error err = da.GroupCreate(ctx, rest.Group{Name: "test-perms"}) @@ -252,7 +253,7 @@ func testUserPermissions(t *testing.T) { if !assert.NoError(t, err) { t.FailNow() } - err = da.GroupAddUser(ctx, "test-perms", "test-perms") + err = da.GroupUserAdd(ctx, "test-perms", "test-perms") if !assert.NoError(t, err) { t.FailNow() } @@ -283,12 +284,12 @@ func testUserPermissions(t *testing.T) { // Expected: a sorted list of strings expected := []string{"test:test-perms-0", "test:test-perms-1", "test:test-perms-2"} - actual, err := da.UserPermissions(ctx, "test-perms") + actual, err := da.UserPermissionList(ctx, "test-perms") if !assert.NoError(t, err) { t.FailNow() } - assert.Equal(t, expected, actual) + assert.Equal(t, expected, actual.Strings()) } func testUserUpdate(t *testing.T) { @@ -307,7 +308,7 @@ func testUserUpdate(t *testing.T) { // Get the user we just added. Emails should match. user1, _ := da.UserGet(ctx, "test-update") if userA.Email != user1.Email { - t.Errorf("Email mistatch: %q vs %q", userA.Email, user1.Email) + t.Errorf("Email mismatch: %q vs %q", userA.Email, user1.Email) t.FailNow() } @@ -319,7 +320,7 @@ func testUserUpdate(t *testing.T) { // Get the user we just updated. Emails should match. user2, _ := da.UserGet(ctx, "test-update") if userB.Email != user2.Email { - t.Errorf("Email mistatch: %q vs %q", userB.Email, user2.Email) + t.Errorf("Email mismatch: %q vs %q", userB.Email, user2.Email) t.FailNow() } } diff --git a/service/bundle-handlers.go b/service/bundle-handlers.go index 24a3504..6f2f9c5 100644 --- a/service/bundle-handlers.go +++ b/service/bundle-handlers.go @@ -59,7 +59,7 @@ func handleGetBundleVersions(w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) name := params["name"] - bundles, err := dataAccessLayer.BundleListVersions(r.Context(), name) + bundles, err := dataAccessLayer.BundleVersionList(r.Context(), name) if err != nil { respondAndLogError(r.Context(), w, err) return diff --git a/service/group-handlers.go b/service/group-handlers.go index 4ae2093..f776cf5 100644 --- a/service/group-handlers.go +++ b/service/group-handlers.go @@ -67,7 +67,7 @@ func handleDeleteGroupMember(w http.ResponseWriter, r *http.Request) { return } - err = dataAccessLayer.GroupRemoveUser(r.Context(), groupname, username) + err = dataAccessLayer.GroupUserDelete(r.Context(), groupname, username) if err != nil { respondAndLogError(r.Context(), w, err) } @@ -182,7 +182,7 @@ func handleGetGroupRoles(w http.ResponseWriter, r *http.Request) { return } - roles, err := dataAccessLayer.GroupListRoles(r.Context(), groupname) + roles, err := dataAccessLayer.GroupRoleList(r.Context(), groupname) if err != nil { respondAndLogError(r.Context(), w, err) return @@ -253,7 +253,7 @@ func handlePutGroupMember(w http.ResponseWriter, r *http.Request) { return } - err = dataAccessLayer.GroupAddUser(r.Context(), groupname, username) + err = dataAccessLayer.GroupUserAdd(r.Context(), groupname, username) if err != nil { respondAndLogError(r.Context(), w, err) } diff --git a/service/role-handlers.go b/service/role-handlers.go index 6f7c48b..9532376 100644 --- a/service/role-handlers.go +++ b/service/role-handlers.go @@ -19,7 +19,9 @@ package service import ( "encoding/json" "net/http" + "sync" + "github.com/getgort/gort/data/rest" "github.com/gorilla/mux" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" ) @@ -37,7 +39,6 @@ func handleDeleteRole(w http.ResponseWriter, r *http.Request) { // handleGetRoles handles "GET /v2/roles" func handleGetRoles(w http.ResponseWriter, r *http.Request) { - roles, err := dataAccessLayer.RoleList(r.Context()) if err != nil { @@ -63,13 +64,80 @@ func handleGetRole(w http.ResponseWriter, r *http.Request) { return } - role, err := dataAccessLayer.RoleGet(r.Context(), rolename) + var errs chan error = make(chan error, 3) + var role rest.Role + var perms rest.RolePermissionList + var groups []rest.Group + + wg := sync.WaitGroup{} + wg.Add(3) + + go func() { + o, err := dataAccessLayer.RoleGet(r.Context(), rolename) + if err != nil { + errs <- err + } + + role = o + wg.Done() + }() + + go func() { + o, err := dataAccessLayer.RolePermissionList(r.Context(), rolename) + if err != nil { + errs <- err + } + + perms = o + wg.Done() + }() + + go func() { + o, err := dataAccessLayer.RoleGroupList(r.Context(), rolename) + if err != nil { + errs <- err + } + + groups = o + wg.Done() + }() + + wg.Wait() + + close(errs) + + if err := <-errs; err != nil { + respondAndLogError(r.Context(), w, err) + } + + role.Permissions = perms + role.Groups = groups + + json.NewEncoder(w).Encode(role) +} + +// handleGetRole handles "GET /v2/roles/{rolename}" +func handleGetRolePermissions(w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + rolename := params["rolename"] + + exists, err := dataAccessLayer.RoleExists(r.Context(), rolename) + if err != nil { + respondAndLogError(r.Context(), w, err) + return + } + if !exists { + http.Error(w, "no such role", http.StatusNotFound) + return + } + + rpl, err := dataAccessLayer.RolePermissionList(r.Context(), rolename) if err != nil { respondAndLogError(r.Context(), w, err) return } - json.NewEncoder(w).Encode(role) + json.NewEncoder(w).Encode(rpl) } // handleGrantRolePermission handles "PUT /v2/roles/{rolename}/bundles/{bundlename}/permissions/{permissionname}" @@ -135,11 +203,12 @@ func handlePutRole(w http.ResponseWriter, r *http.Request) { func addRoleMethodsToRouter(router *mux.Router) { // Basic role methods router.Handle("/v2/roles", otelhttp.NewHandler(authCommand(handleGetRoles, "role", "list"), "handleGetRoles")).Methods("GET") - router.Handle("/v2/roles/{rolename}", otelhttp.NewHandler(authCommand(handleGetRole, "role", "infp"), "handleGetRole")).Methods("GET") + router.Handle("/v2/roles/{rolename}", otelhttp.NewHandler(authCommand(handleGetRole, "role", "info"), "handleGetRole")).Methods("GET") router.Handle("/v2/roles/{rolename}", otelhttp.NewHandler(authCommand(handlePutRole, "role", "create"), "handlePutRole")).Methods("PUT") router.Handle("/v2/roles/{rolename}", otelhttp.NewHandler(authCommand(handleDeleteRole, "role", "delete"), "handleDeleteRole")).Methods("DELETE") // Role permissions + router.Handle("/v2/roles/{rolename}/permissions", otelhttp.NewHandler(authCommand(handleGetRolePermissions, "role", "info"), "handleGetRolePermissions")).Methods("GET") router.Handle("/v2/roles/{rolename}/bundles/{bundlename}/permissions/{permissionname}", otelhttp.NewHandler(authCommand(handleRevokeRolePermission, "role", "revoke-permission"), "handleDeleteRolePermission")).Methods("DELETE") router.Handle("/v2/roles/{rolename}/bundles/{bundlename}/permissions/{permissionname}", otelhttp.NewHandler(authCommand(handleGrantRolePermission, "role", "grant-permission"), "handlePutRolePermission")).Methods("PUT") } diff --git a/service/service.go b/service/service.go index 8a6ee97..71171c5 100644 --- a/service/service.go +++ b/service/service.go @@ -317,7 +317,7 @@ func doBootstrap(ctx context.Context, user rest.User) (rest.User, error) { } // Add the admin user to the admin group. - err = dataAccessLayer.GroupAddUser(ctx, adminGroup, user.Username) + err = dataAccessLayer.GroupUserAdd(ctx, adminGroup, user.Username) if err != nil { return user, err } @@ -443,6 +443,8 @@ func respondAndLogError(ctx context.Context, w http.ResponseWriter, err error) { fallthrough case gerrs.Is(err, errs.ErrNoSuchGroup): fallthrough + case gerrs.Is(err, errs.ErrNoSuchRole): + fallthrough case gerrs.Is(err, errs.ErrNoSuchToken): fallthrough case gerrs.Is(err, errs.ErrNoSuchUser): @@ -589,7 +591,7 @@ func doAuthenticateUser(r *http.Request, gortCommand string, args ...string) (bo return false, err } - perms, err := dataAccessLayer.UserPermissions(r.Context(), token.User) + perms, err := dataAccessLayer.UserPermissionList(r.Context(), token.User) if err != nil { return false, err } @@ -609,7 +611,7 @@ func doAuthenticateUser(r *http.Request, gortCommand string, args ...string) (bo env := rules.EvaluationEnvironment{"arg": argValues} - return auth.EvaluateCommandEntry(perms, ce, env) + return auth.EvaluateCommandEntry(perms.Strings(), ce, env) } // getGortBundleCommand retrieves the data.BundleCommand value from the default diff --git a/service/user-handlers.go b/service/user-handlers.go index dce3e9f..5d7156a 100644 --- a/service/user-handlers.go +++ b/service/user-handlers.go @@ -100,6 +100,19 @@ func handleGetUsers(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(users) } +// handleGetUserPermissions handles "GET /v2/users/{username}/groups" +func handleGetUserPermissions(w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + + perms, err := dataAccessLayer.UserPermissionList(r.Context(), params["username"]) + if err != nil { + respondAndLogError(r.Context(), w, err) + return + } + + json.NewEncoder(w).Encode(perms) +} + // handlePutUser handles "POST /v2/users/{username}" func handlePutUser(w http.ResponseWriter, r *http.Request) { var user rest.User @@ -148,4 +161,7 @@ func addUserMethodsToRouter(router *mux.Router) { router.Handle("/v2/users/{username}/groups", otelhttp.NewHandler(authCommand(handleGetUserGroups, "user", "info"), "handleGetUserGroups")).Methods("GET") router.Handle("/v2/users/{username}/groups/{username}", otelhttp.NewHandler(authCommand(handleDeleteUserGroup, "user", "update"), "handleDeleteUserGroup")).Methods("DELETE") router.Handle("/v2/users/{username}/groups/{username}", otelhttp.NewHandler(authCommand(handlePutUserGroup, "user", "update"), "handlePutUserGroup")).Methods("PUT") + + // User permissions list + router.Handle("/v2/users/{username}/permissions", otelhttp.NewHandler(authCommand(handleGetUserPermissions, "user", "info"), "handleGetUserPermissions")).Methods("GET") } diff --git a/version/version.go b/version/version.go index d38de25..b2f3cd0 100644 --- a/version/version.go +++ b/version/version.go @@ -18,5 +18,5 @@ package version const ( // Version is the current version of Gort - Version = "0.8.0-alpha.1" + Version = "0.8.0-alpha.2" )