diff --git a/cmd/projects.go b/cmd/projects.go index 478623c..d7601ea 100644 --- a/cmd/projects.go +++ b/cmd/projects.go @@ -1,97 +1,160 @@ package cmd import ( + "errors" "fmt" "os" "strings" "text/tabwriter" + "valar/cli/pkg/api" "valar/cli/pkg/config" "github.com/spf13/cobra" ) var ( - authCmd = &cobra.Command{ - Use: "auth", - Short: "Manage project permissions", + authListCmd = &cobra.Command{ + Use: "list [path]", + Short: "List permissions for a path prefix", + Args: cobra.MaximumNArgs(1), Run: runAndHandle(func(cmd *cobra.Command, args []string) error { client, err := globalConfiguration.APIClient() if err != nil { return err } - var project string cfg := &config.ServiceConfig{} - if err := cfg.ReadFromFile(functionConfiguration); err != nil { - // Use default project - project = globalConfiguration.Project() - } else { - project = cfg.Project + if err := cfg.ReadFromFile(functionConfiguration); errors.Is(err, os.ErrNotExist) { + cfg.Project = globalConfiguration.Project() + } else if err != nil { + return err } - pms, err := client.ListPermissions(project) + namespace, prefix := "service", cfg.Project + if len(args) == 1 { + subargs := strings.SplitN(args[0], ":", 2) + if len(subargs) != 2 { + return fmt.Errorf("expect path to be in the form namespace:prefix") + } + namespace, prefix = subargs[0], subargs[1] + } + permissions, err := client.ListPermissions(cfg.Project, namespace, prefix) if err != nil { return err } tw := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0) - fmt.Fprintln(tw, "USER\tACTIONS") - for user, actions := range pms { - fmt.Fprintf(tw, "%s\t%s\n", user, strings.Join(actions, ", ")) + fmt.Fprintln(tw, "PATH\tUSER\tACTION\tSTATE") + for _, pm := range permissions { + fmt.Fprintf(tw, "%s\t%s\t%s\t%s\n", pm.Path, pm.User, pm.Action, pm.State) } tw.Flush() return nil }), } - authUser, authAction string - authAllowCmd = &cobra.Command{ - Use: "allow", - Short: "Allow a user to perform a specific action", - Args: cobra.ExactArgs(0), + + authAllowCmd = &cobra.Command{ + Use: "allow path user action", + Short: "Modify permissions for a path and user", + Args: cobra.ExactArgs(3), + Run: runAndHandle(authModifyWithState("allow")), + } + authForbidCmd = &cobra.Command{ + Use: "forbid path user action", + Short: "Forbid a specific action for a path and user", + Args: cobra.ExactArgs(3), + Run: runAndHandle(authModifyWithState("forbid")), + } + authClearCmd = &cobra.Command{ + Use: "clear path user action", + Short: "Remove the permission for a path and user", + Args: cobra.ExactArgs(3), + Run: runAndHandle(authModifyWithState("unset")), + } + authCheckCmd = &cobra.Command{ + Use: "check path user action", + Short: "Check if a user can perform an action", + Args: cobra.ExactArgs(3), Run: runAndHandle(func(cmd *cobra.Command, args []string) error { client, err := globalConfiguration.APIClient() if err != nil { return err } - var project string cfg := &config.ServiceConfig{} - if err := cfg.ReadFromFile(functionConfiguration); err != nil { - project = globalConfiguration.Project() - } else { - project = cfg.Project + if err := cfg.ReadFromFile(functionConfiguration); errors.Is(err, os.ErrNotExist) { + cfg.Project = globalConfiguration.Project() + } else if err != nil { + return err } - if err := client.ModifyPermission(project, authUser, authAction, false); err != nil { + path, err := api.PermissionPathFromString(args[0]) + if err != nil { return err } - return nil - }), - } - authForbidCmd = &cobra.Command{ - Use: "forbid", - Short: "Forbid a user to perform a specific action", - Args: cobra.ExactArgs(0), - Run: runAndHandle(func(cmd *cobra.Command, args []string) error { - client, err := globalConfiguration.APIClient() + user, err := api.PermissionUserFromString(args[1]) if err != nil { return err } - var project string - cfg := &config.ServiceConfig{} - if err := cfg.ReadFromFile(functionConfiguration); err != nil { - project = globalConfiguration.Project() - } else { - project = cfg.Project + permission := api.Permission{ + Path: path, + User: user, + Action: args[2], } - if err := client.ModifyPermission(project, authUser, authAction, true); err != nil { + allowed, err := client.CheckPermission(cfg.Project, permission) + if err != nil { return err } + if allowed { + fmt.Println("allowed") + } else { + fmt.Println("forbidden") + } return nil }), } + + authCmd = &cobra.Command{ + Use: "auth", + Short: "Manage user and service permissions", + } ) +func authModifyWithState(state string) func(cmd *cobra.Command, args []string) error { + return func(cmd *cobra.Command, args []string) error { + client, err := globalConfiguration.APIClient() + if err != nil { + return err + } + cfg := &config.ServiceConfig{} + if err := cfg.ReadFromFile(functionConfiguration); errors.Is(err, os.ErrNotExist) { + cfg.Project = globalConfiguration.Project() + } else if err != nil { + return err + } + path, err := api.PermissionPathFromString(args[0]) + if err != nil { + return err + } + user, err := api.PermissionUserFromString(args[1]) + if err != nil { + return err + } + permission := api.Permission{ + Path: path, + User: user, + Action: args[2], + State: state, + } + modified, err := client.ModifyPermission(cfg.Project, permission) + if err != nil { + return err + } + if modified { + fmt.Println("modified") + } else { + fmt.Println("unchanged") + } + return nil + } +} + func initProjectsCmd() { - authCmd.AddCommand(authAllowCmd, authForbidCmd) - authForbidCmd.Flags().StringVarP(&authAction, "action", "a", "invoke", "Action to be modified") - authForbidCmd.Flags().StringVarP(&authUser, "user", "u", "anonymous", "User to be modified") - authAllowCmd.Flags().StringVarP(&authAction, "action", "a", "invoke", "Action to be modified") - authAllowCmd.Flags().StringVarP(&authUser, "user", "u", "anonymous", "User to be modified") + authCmd.AddCommand(authListCmd, authAllowCmd, authForbidCmd, authClearCmd, authCheckCmd) rootCmd.AddCommand(authCmd) } diff --git a/pkg/api/client.go b/pkg/api/client.go index 137b9d2..1d575f0 100644 --- a/pkg/api/client.go +++ b/pkg/api/client.go @@ -8,6 +8,7 @@ import ( "net/http" "net/url" "strconv" + "strings" "time" ) @@ -225,10 +226,13 @@ func (client *Client) ListBuilds(project, service, id string) ([]Build, error) { } // ListPermissions retrieves all permissions set for a specific project. -func (client *Client) ListPermissions(project string) (PermissionSet, error) { +func (client *Client) ListPermissions(project, namespace, prefix string) ([]Permission, error) { + params := url.Values{} + params.Add("namespace", namespace) + params.Add("prefix", prefix) var ( - set = make(PermissionSet) - path = fmt.Sprintf("/projects/%s/auth", project) + set = []Permission{} + path = fmt.Sprintf("/projects/%s/permissions?%s", project, params.Encode()) ) if err := client.request(http.MethodGet, path, &set, nil); err != nil { return nil, err @@ -237,19 +241,35 @@ func (client *Client) ListPermissions(project string) (PermissionSet, error) { } // ModifyPermission modifies the permission set of a project. -func (client *Client) ModifyPermission(project, user, action string, forbid bool) error { +func (client *Client) ModifyPermission(project string, permission Permission) (bool, error) { var ( - resp struct{} - path = fmt.Sprintf("/projects/%s/auth/%s/%s", project, user, action) + resp struct { + Modified bool `json:"modified"` + } + path = fmt.Sprintf("/projects/%s/permissions", project) method = http.MethodPost ) - if forbid { - method = http.MethodDelete + body, _ := json.Marshal(&permission) + if err := client.request(method, path, &resp, bytes.NewReader(body)); err != nil { + return false, err } - if err := client.request(method, path, &resp, nil); err != nil { - return err + return resp.Modified, nil +} + +// CheckPermission checks if a user has a specific permission. +func (client *Client) CheckPermission(project string, permission Permission) (bool, error) { + var ( + resp struct { + Allowed bool `json:"allowed"` + } + path = fmt.Sprintf("/projects/%s/permissions?mode=check", project) + method = http.MethodPost + ) + body, _ := json.Marshal(&permission) + if err := client.request(method, path, &resp, bytes.NewReader(body)); err != nil { + return false, err } - return nil + return resp.Allowed, nil } // ListDeployments shows all deployments of a specific service. @@ -419,7 +439,52 @@ type KVPair struct { Secret bool `json:"secret"` } -type PermissionSet map[string][]string +type PermissionPath struct { + Namespace string `json:"namespace"` + Items []string `json:"items"` +} + +func (pp PermissionPath) String() string { + return fmt.Sprintf("%s:%s", pp.Namespace, strings.Join(pp.Items, "/")) +} + +func PermissionPathFromString(str string) (PermissionPath, error) { + path := PermissionPath{} + parts := strings.SplitN(str, ":", 2) + if len(parts) != 2 { + return path, fmt.Errorf("invalid path: %s", str) + } + path.Namespace = parts[0] + path.Items = strings.Split(parts[1], "/") + return path, nil +} + +type PermissionUser struct { + Type string `json:"type"` + Identifier []string `json:"identifier"` +} + +func (pp PermissionUser) String() string { + return fmt.Sprintf("%s:%s", pp.Type, strings.Join(pp.Identifier, "/")) +} + +func PermissionUserFromString(str string) (PermissionUser, error) { + user := PermissionUser{} + parts := strings.SplitN(str, ":", 2) + if len(parts) != 2 { + return user, fmt.Errorf("invalid user: %s", str) + } + user.Type = parts[0] + user.Identifier = strings.Split(parts[1], "/") + return user, nil +} + +type Permission struct { + Path PermissionPath `json:"path"` + User PermissionUser `json:"user"` + Action string `json:"action"` + State string `json:"state"` +} type UserInfo struct { Name string `json:"name"`