diff --git a/README.md b/README.md index f5d487e..d464d15 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ Admin CLI: - `fs admin user set-status backup-user --status disabled` - `fs admin user set-role backup-user --role readonly --bucket backup-bucket --prefix restic/` - `fs admin user set-role backup-user --role readwrite --bucket backups-2` (appends another statement) +- `fs admin user remove-role backup-user --role readonly --bucket backup-bucket --prefix restic/` - `fs admin user set-role backup-user --role admin --replace` (replaces all statements) - `fs admin user delete backup-user` - `fs admin diag health` diff --git a/cmd/admin_user.go b/cmd/admin_user.go index 4094828..3f24a7e 100644 --- a/cmd/admin_user.go +++ b/cmd/admin_user.go @@ -22,6 +22,7 @@ func newAdminUserCommand(opts *adminOptions) *cobra.Command { cmd.AddCommand(newAdminUserDeleteCommand(opts)) cmd.AddCommand(newAdminUserSetStatusCommand(opts)) cmd.AddCommand(newAdminUserSetRoleCommand(opts)) + cmd.AddCommand(newAdminUserRemoveRoleCommand(opts)) return cmd } @@ -253,6 +254,68 @@ func newAdminUserSetRoleCommand(opts *adminOptions) *cobra.Command { return cmd } +func newAdminUserRemoveRoleCommand(opts *adminOptions) *cobra.Command { + var ( + role string + bucket string + prefix string + ) + cmd := &cobra.Command{ + Use: "remove-role ", + Short: "Remove one role policy statement from user", + Args: requireAccessKeyArg("fs admin user remove-role --role admin|readwrite|readonly [--bucket ] [--prefix ]"), + RunE: func(cmd *cobra.Command, args []string) error { + policy, err := buildPolicyFromRole(rolePolicyOptions{ + Role: role, + Bucket: bucket, + Prefix: prefix, + }) + if err != nil { + return usageError("fs admin user remove-role --role admin|readwrite|readonly [--bucket ] [--prefix ]", err.Error()) + } + if len(policy.Statements) == 0 { + return usageError("fs admin user remove-role --role admin|readwrite|readonly [--bucket ] [--prefix ]", "no statement to remove") + } + + client, err := newAdminAPIClient(opts, true) + if err != nil { + return err + } + + existing, err := client.GetUser(context.Background(), args[0]) + if err != nil { + return err + } + if existing.Policy == nil || len(existing.Policy.Statements) == 0 { + return fmt.Errorf("user %q has no policy statements", args[0]) + } + + target := policy.Statements[0] + nextPolicy, removed := removePolicyStatements(existing.Policy, target) + if removed == 0 { + return fmt.Errorf("no matching statement found for role=%s bucket=%s prefix=%s", role, bucket, prefix) + } + if len(nextPolicy.Statements) == 0 { + return fmt.Errorf("cannot remove the last policy statement; add another role first or use set-role --replace") + } + + out, err := client.SetUserPolicy(context.Background(), args[0], nextPolicy) + if err != nil { + return err + } + if opts.JSON { + return writeJSON(cmd.OutOrStdout(), out) + } + return writeUserTable(cmd.OutOrStdout(), out, false) + }, + } + + cmd.Flags().StringVar(&role, "role", "readwrite", "Role: admin|readwrite|readonly") + cmd.Flags().StringVar(&bucket, "bucket", "*", "Bucket scope, defaults to *") + cmd.Flags().StringVar(&prefix, "prefix", "*", "Prefix scope, defaults to *") + return cmd +} + func mergePolicyStatements(existing *adminPolicy, addition adminPolicy) adminPolicy { merged := adminPolicy{} if existing != nil { @@ -289,6 +352,25 @@ func policyStatementsEqual(a, b adminPolicyStatement) bool { return true } +func removePolicyStatements(existing *adminPolicy, target adminPolicyStatement) (adminPolicy, int) { + out := adminPolicy{} + if existing == nil { + return out, 0 + } + out.Principal = existing.Principal + out.Statements = make([]adminPolicyStatement, 0, len(existing.Statements)) + + removed := 0 + for _, stmt := range existing.Statements { + if policyStatementsEqual(stmt, target) { + removed++ + continue + } + out.Statements = append(out.Statements, stmt) + } + return out, removed +} + func requireAccessKeyArg(usage string) cobra.PositionalArgs { return func(cmd *cobra.Command, args []string) error { if len(args) != 1 {