github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/idp-ldap-policy-subcommands.go (about)

     1  // Copyright (c) 2015-2023 MinIO, Inc.
     2  //
     3  // This file is part of MinIO Object Storage stack
     4  //
     5  // This program is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU Affero General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  //
    10  // This program is distributed in the hope that it will be useful
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  // GNU Affero General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Affero General Public License
    16  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17  
    18  package cmd
    19  
    20  import (
    21  	"errors"
    22  	"fmt"
    23  	"strings"
    24  	"time"
    25  
    26  	"github.com/charmbracelet/lipgloss"
    27  	"github.com/minio/cli"
    28  	json "github.com/minio/colorjson"
    29  	"github.com/minio/madmin-go/v3"
    30  	"github.com/minio/mc/pkg/probe"
    31  )
    32  
    33  var idpLdapPolicyAttachFlags = []cli.Flag{
    34  	cli.StringFlag{
    35  		Name:  "user, u",
    36  		Usage: "attach policy to user by DN or by login name",
    37  	},
    38  	cli.StringFlag{
    39  		Name:  "group, g",
    40  		Usage: "attach policy to LDAP Group DN",
    41  	},
    42  }
    43  
    44  var idpLdapPolicyAttachCmd = cli.Command{
    45  	Name:         "attach",
    46  	Usage:        "attach a policy to an entity",
    47  	Action:       mainIDPLdapPolicyAttach,
    48  	Before:       setGlobalsFromContext,
    49  	Flags:        append(idpLdapPolicyAttachFlags, globalFlags...),
    50  	OnUsageError: onUsageError,
    51  	CustomHelpTemplate: `NAME:
    52    {{.HelpName}} - {{.Usage}}
    53  
    54  USAGE:
    55    {{.HelpName}} [FLAGS] TARGET POLICY [POLICY...] [ --user=USER | --group=GROUP ]
    56  
    57    Exactly one "--user" or "--group" flag is required.
    58  
    59  POLICY:
    60    Name of a policy on the MinIO server.
    61  
    62  FLAGS:
    63    {{range .VisibleFlags}}{{.}}
    64    {{end}}
    65  EXAMPLES:
    66    1. Attach policy "mypolicy" to a user
    67       {{.Prompt}} {{.HelpName}} play/ mypolicy --user='uid=bobfisher,ou=people,ou=hwengg,dc=min,dc=io'
    68    2. Attach policies "policy1" and "policy2" to a group
    69       {{.Prompt}} {{.HelpName}} play/ policy1 policy2 --group='cn=projectb,ou=groups,ou=swengg,dc=min,dc=io'
    70  `,
    71  }
    72  
    73  // Quote from AWS policy naming requirement (ref:
    74  // https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_iam-quotas.html):
    75  //
    76  // Names of users, groups, roles, policies, instance profiles, and server
    77  // certificates must be alphanumeric, including the following common characters:
    78  // plus (+), equal (=), comma (,), period (.), at (@), underscore (_), and
    79  // hyphen (-).
    80  
    81  func mainIDPLdapPolicyAttach(ctx *cli.Context) error {
    82  	// We need exactly one alias, and at least one policy.
    83  	if len(ctx.Args()) < 2 {
    84  		showCommandHelpAndExit(ctx, 1)
    85  	}
    86  	user := ctx.String("user")
    87  	group := ctx.String("group")
    88  
    89  	args := ctx.Args()
    90  	aliasedURL := args.Get(0)
    91  
    92  	policies := args[1:]
    93  	req := madmin.PolicyAssociationReq{
    94  		Policies: policies,
    95  		User:     user,
    96  		Group:    group,
    97  	}
    98  	fatalIf(probe.NewError(req.IsValid()), "Invalid policy attach arguments.")
    99  
   100  	// Create a new MinIO Admin Client
   101  	client, err := newAdminClient(aliasedURL)
   102  	fatalIf(err, "Unable to initialize admin connection.")
   103  
   104  	res, e := client.AttachPolicyLDAP(globalContext, req)
   105  	fatalIf(probe.NewError(e), "Unable to make LDAP policy association")
   106  
   107  	m := policyAssociationMessage{
   108  		attach:           true,
   109  		Status:           "success",
   110  		PoliciesAttached: res.PoliciesAttached,
   111  		User:             user,
   112  		Group:            group,
   113  	}
   114  	printMsg(m)
   115  	return nil
   116  }
   117  
   118  type policyAssociationMessage struct {
   119  	attach           bool
   120  	Status           string   `json:"status"`
   121  	PoliciesAttached []string `json:"policiesAttached,omitempty"`
   122  	PoliciesDetached []string `json:"policiesDetached,omitempty"`
   123  	User             string   `json:"user,omitempty"`
   124  	Group            string   `json:"group,omitempty"`
   125  }
   126  
   127  func (m policyAssociationMessage) String() string {
   128  	style := lipgloss.NewStyle().Foreground(lipgloss.Color("#04B575")) // green
   129  
   130  	policiesS := style.Render("Attached Policies:")
   131  	entityS := style.Render("To User:")
   132  	policies := m.PoliciesAttached
   133  	entity := m.User
   134  	switch {
   135  	case m.User != "" && m.attach:
   136  	case m.User != "" && !m.attach:
   137  		policiesS = style.Render("Detached Policies:")
   138  		policies = m.PoliciesDetached
   139  		entityS = style.Render("From User:")
   140  	case m.Group != "" && m.attach:
   141  		entityS = style.Render("To Group:")
   142  		entity = m.Group
   143  	case m.Group != "" && !m.attach:
   144  		policiesS = style.Render("Detached Policies:")
   145  		policies = m.PoliciesDetached
   146  		entityS = style.Render("From Group:")
   147  		entity = m.Group
   148  	}
   149  	return fmt.Sprintf("%s %v\n%s %s\n", policiesS, policies, entityS, entity)
   150  }
   151  
   152  func (m policyAssociationMessage) JSON() string {
   153  	jsonMessageBytes, e := json.MarshalIndent(m, "", " ")
   154  	fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
   155  
   156  	return string(jsonMessageBytes)
   157  }
   158  
   159  var idpLdapPolicyDetachFlags = []cli.Flag{
   160  	cli.StringFlag{
   161  		Name:  "user, u",
   162  		Usage: "attach policy to user by DN or by login name",
   163  	},
   164  	cli.StringFlag{
   165  		Name:  "group, g",
   166  		Usage: "attach policy to LDAP Group DN",
   167  	},
   168  }
   169  
   170  var idpLdapPolicyDetachCmd = cli.Command{
   171  	Name:         "detach",
   172  	Usage:        "detach a policy from an entity",
   173  	Action:       mainIDPLdapPolicyDetach,
   174  	Before:       setGlobalsFromContext,
   175  	Flags:        append(idpLdapPolicyDetachFlags, globalFlags...),
   176  	OnUsageError: onUsageError,
   177  	CustomHelpTemplate: `NAME:
   178    {{.HelpName}} - {{.Usage}}
   179  
   180  USAGE:
   181    {{.HelpName}} [FLAGS] TARGET POLICY [POLICY...] [ --user=USER | --group=GROUP ]
   182  
   183    Exactly one of "--user" or "--group" is required.
   184  
   185  POLICY:
   186    Name of a policy on the MinIO server.
   187  
   188  FLAGS:
   189    {{range .VisibleFlags}}{{.}}
   190    {{end}}
   191  EXAMPLES:
   192    1. Detach policy "mypolicy" from a user
   193       {{.Prompt}} {{.HelpName}} play/ mypolicy --user='uid=bobfisher,ou=people,ou=hwengg,dc=min,dc=io'
   194    2. Detach policies "policy1" and "policy2" from a group
   195       {{.Prompt}} {{.HelpName}} play/ policy1 policy2 --group='cn=projectb,ou=groups,ou=swengg,dc=min,dc=io'
   196  `,
   197  }
   198  
   199  // Quote from AWS policy naming requirement (ref:
   200  // https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_iam-quotas.html):
   201  //
   202  // Names of users, groups, roles, policies, instance profiles, and server
   203  // certificates must be alphanumeric, including the following common characters:
   204  // plus (+), equal (=), comma (,), period (.), at (@), underscore (_), and
   205  // hyphen (-).
   206  
   207  func mainIDPLdapPolicyDetach(ctx *cli.Context) error {
   208  	// We need exactly one alias, and at least one policy.
   209  	if len(ctx.Args()) < 2 {
   210  		showCommandHelpAndExit(ctx, 1)
   211  	}
   212  
   213  	user := ctx.String("user")
   214  	group := ctx.String("group")
   215  
   216  	if user == "" && group == "" {
   217  		e := errors.New("at least one of --user or --group is required.")
   218  		fatalIf(probe.NewError(e), "Missing flag in command")
   219  	}
   220  
   221  	args := ctx.Args()
   222  	aliasedURL := args.Get(0)
   223  
   224  	policies := args[1:]
   225  
   226  	// Create a new MinIO Admin Client
   227  	client, err := newAdminClient(aliasedURL)
   228  	fatalIf(err, "Unable to initialize admin connection.")
   229  
   230  	res, e := client.DetachPolicyLDAP(globalContext,
   231  		madmin.PolicyAssociationReq{
   232  			Policies: policies,
   233  			User:     user,
   234  			Group:    group,
   235  		})
   236  	fatalIf(probe.NewError(e), "Unable to make LDAP policy association")
   237  
   238  	m := policyAssociationMessage{
   239  		attach:           false,
   240  		Status:           "success",
   241  		PoliciesDetached: res.PoliciesDetached,
   242  		User:             user,
   243  		Group:            group,
   244  	}
   245  	printMsg(m)
   246  	return nil
   247  }
   248  
   249  var idpLdapPolicyEntitiesFlags = []cli.Flag{
   250  	cli.StringSliceFlag{
   251  		Name:  "user, u",
   252  		Usage: "list policies associated with user(s)",
   253  	},
   254  	cli.StringSliceFlag{
   255  		Name:  "group, g",
   256  		Usage: "list policies associated with group(s)",
   257  	},
   258  	cli.StringSliceFlag{
   259  		Name:  "policy, p",
   260  		Usage: "list users or groups associated with policy",
   261  	},
   262  }
   263  
   264  var idpLdapPolicyEntitiesCmd = cli.Command{
   265  	Name:         "entities",
   266  	Usage:        "list policy association entities",
   267  	Action:       mainIDPLdapPolicyEntities,
   268  	Before:       setGlobalsFromContext,
   269  	Flags:        append(idpLdapPolicyEntitiesFlags, globalFlags...),
   270  	OnUsageError: onUsageError,
   271  	CustomHelpTemplate: `NAME:
   272    {{.HelpName}} - {{.Usage}}
   273  
   274  USAGE:
   275    {{.HelpName}} [FLAGS] TARGET
   276  
   277  FLAGS:
   278    {{range .VisibleFlags}}{{.}}
   279    {{end}}
   280  EXAMPLES:
   281    1. List all LDAP entities associated with all policies
   282       {{.Prompt}} {{.HelpName}} play/
   283    2. List all LDAP entities associated with the policies 'finteam-policy' and 'mlteam-policy'
   284       {{.Prompt}} {{.HelpName}} play/ --policy finteam-policy --policy mlteam-policy
   285    3. List all policies associated with a pair of User LDAP entities
   286       {{.Prompt}} {{.HelpName}} play/ \
   287                --user 'uid=bobfisher,ou=people,ou=hwengg,dc=min,dc=io' \
   288                --user 'uid=fahim,ou=people,ou=swengg,dc=min,dc=io'
   289    4. List all policies associated with a pair of Group LDAP entities
   290       {{.Prompt}} {{.HelpName}} play/ \
   291                --group 'cn=projecta,ou=groups,ou=swengg,dc=min,dc=io' \
   292                --group 'cn=projectb,ou=groups,ou=swengg,dc=min,dc=io'
   293    5. List all entities associated with a policy, group and user
   294       {{.Prompt}} {{.HelpName}} play/ \
   295                --policy finteam-policy
   296                --user 'uid=bobfisher,ou=people,ou=hwengg,dc=min,dc=io' \
   297                --group 'cn=projectb,ou=groups,ou=swengg,dc=min,dc=io'
   298  `,
   299  }
   300  
   301  func mainIDPLdapPolicyEntities(ctx *cli.Context) error {
   302  	if len(ctx.Args()) != 1 {
   303  		showCommandHelpAndExit(ctx, 1)
   304  	}
   305  
   306  	usersToQuery := ctx.StringSlice("user")
   307  	groupsToQuery := ctx.StringSlice("group")
   308  	policiesToQuery := ctx.StringSlice("policy")
   309  
   310  	args := ctx.Args()
   311  
   312  	aliasedURL := args.Get(0)
   313  
   314  	// Create a new MinIO Admin Client
   315  	client, err := newAdminClient(aliasedURL)
   316  	fatalIf(err, "Unable to initialize admin connection.")
   317  
   318  	res, e := client.GetLDAPPolicyEntities(globalContext,
   319  		madmin.PolicyEntitiesQuery{
   320  			Users:  usersToQuery,
   321  			Groups: groupsToQuery,
   322  			Policy: policiesToQuery,
   323  		})
   324  	fatalIf(probe.NewError(e), "Unable to fetch LDAP policy entities")
   325  
   326  	printMsg(policyEntitiesFrom(res))
   327  	return nil
   328  }
   329  
   330  type policyEntities struct {
   331  	Status string                      `json:"status"`
   332  	Result madmin.PolicyEntitiesResult `json:"result"`
   333  }
   334  
   335  func policyEntitiesFrom(r madmin.PolicyEntitiesResult) policyEntities {
   336  	return policyEntities{
   337  		Status: "success",
   338  		Result: r,
   339  	}
   340  }
   341  
   342  func (p policyEntities) JSON() string {
   343  	bs, e := json.MarshalIndent(p, "", "  ")
   344  	fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
   345  
   346  	return string(bs)
   347  }
   348  
   349  func iFmt(n int, fmtStr string, a ...any) string {
   350  	indentStr := ""
   351  	if n > 0 {
   352  		s := make([]rune, n)
   353  		for i := range s {
   354  			s[i] = ' '
   355  		}
   356  		indentStr = string(s)
   357  	}
   358  	return fmt.Sprintf(indentStr+fmtStr, a...)
   359  }
   360  
   361  func (p policyEntities) String() string {
   362  	labelStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#04B575")) // green
   363  	o := strings.Builder{}
   364  
   365  	o.WriteString(iFmt(0, "%s %s\n",
   366  		labelStyle.Render("Query time:"),
   367  		p.Result.Timestamp.Format(time.RFC3339)))
   368  
   369  	if len(p.Result.UserMappings) > 0 {
   370  		o.WriteString(iFmt(0, "%s\n", labelStyle.Render("User -> Policy Mappings:")))
   371  
   372  		for _, u := range p.Result.UserMappings {
   373  			o.WriteString(iFmt(2, "%s %s\n", labelStyle.Render("User:"), u.User))
   374  			for _, p := range u.Policies {
   375  				o.WriteString(iFmt(4, "%s\n", p))
   376  			}
   377  		}
   378  	}
   379  	if len(p.Result.GroupMappings) > 0 {
   380  		o.WriteString(iFmt(0, "%s\n", labelStyle.Render("Group -> Policy Mappings:")))
   381  
   382  		for _, u := range p.Result.GroupMappings {
   383  			o.WriteString(iFmt(2, "%s %s\n", labelStyle.Render("Group:"), u.Group))
   384  			for _, p := range u.Policies {
   385  				o.WriteString(iFmt(4, "%s\n", p))
   386  			}
   387  		}
   388  	}
   389  	if len(p.Result.PolicyMappings) > 0 {
   390  		o.WriteString(iFmt(0, "%s\n", labelStyle.Render("Policy -> Entity Mappings:")))
   391  
   392  		for _, u := range p.Result.PolicyMappings {
   393  			o.WriteString(iFmt(2, "%s %s\n", labelStyle.Render("Policy:"), u.Policy))
   394  			if len(u.Users) > 0 {
   395  				o.WriteString(iFmt(4, "%s\n", labelStyle.Render("User Mappings:")))
   396  				for _, p := range u.Users {
   397  					o.WriteString(iFmt(6, "%s\n", p))
   398  				}
   399  			}
   400  			if len(u.Groups) > 0 {
   401  				o.WriteString(iFmt(4, "%s\n", labelStyle.Render("Group Mappings:")))
   402  				for _, p := range u.Groups {
   403  					o.WriteString(iFmt(6, "%s\n", p))
   404  				}
   405  			}
   406  		}
   407  	}
   408  
   409  	return o.String()
   410  }