github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/idp-openid-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  	"fmt"
    22  	"strings"
    23  
    24  	"github.com/charmbracelet/lipgloss"
    25  	"github.com/minio/cli"
    26  	json "github.com/minio/colorjson"
    27  	"github.com/minio/madmin-go/v3"
    28  	"github.com/minio/mc/pkg/probe"
    29  )
    30  
    31  var idpOpenidAddCmd = cli.Command{
    32  	Name:         "add",
    33  	Usage:        "Create an OpenID IDP server configuration",
    34  	Action:       mainIDPOpenIDAdd,
    35  	Before:       setGlobalsFromContext,
    36  	Flags:        globalFlags,
    37  	OnUsageError: onUsageError,
    38  	CustomHelpTemplate: `NAME:
    39    {{.HelpName}} - {{.Usage}}
    40  
    41  USAGE:
    42    {{.HelpName}} TARGET [CFG_NAME] [CFG_PARAMS...]
    43  
    44  FLAGS:
    45    {{range .VisibleFlags}}{{.}}
    46    {{end}}
    47  EXAMPLES:
    48    1. Create a default OpenID IDP configuration (CFG_NAME is omitted).
    49       {{.Prompt}} {{.HelpName}} play/ \
    50            client_id=minio-client-app \
    51            client_secret=minio-client-app-secret \
    52            config_url="http://localhost:5556/dex/.well-known/openid-configuration" \
    53            scopes="openid,groups" \
    54            redirect_uri="http://127.0.0.1:10000/oauth_callback" \
    55            role_policy="consoleAdmin"
    56    2. Create OpenID IDP configuration named "dex_test".
    57       {{.Prompt}} {{.HelpName}} play/ dex_test \
    58            client_id=minio-client-app \
    59            client_secret=minio-client-app-secret \
    60            config_url="http://localhost:5556/dex/.well-known/openid-configuration" \
    61            scopes="openid,groups" \
    62            redirect_uri="http://127.0.0.1:10000/oauth_callback" \
    63            role_policy="consoleAdmin"
    64  `,
    65  }
    66  
    67  func mainIDPOpenIDAdd(ctx *cli.Context) error {
    68  	return mainIDPOpenIDAddOrUpdate(ctx, false)
    69  }
    70  
    71  func mainIDPOpenIDAddOrUpdate(ctx *cli.Context, update bool) error {
    72  	if len(ctx.Args()) < 2 {
    73  		showCommandHelpAndExit(ctx, 1)
    74  	}
    75  
    76  	args := ctx.Args()
    77  
    78  	aliasedURL := args.Get(0)
    79  
    80  	// Create a new MinIO Admin Client
    81  	client, err := newAdminClient(aliasedURL)
    82  	fatalIf(err, "Unable to initialize admin connection.")
    83  
    84  	cfgName := madmin.Default
    85  	input := args[1:]
    86  	if !strings.Contains(args.Get(1), "=") {
    87  		cfgName = args.Get(1)
    88  		input = args[2:]
    89  	}
    90  
    91  	inputCfg := strings.Join(input, " ")
    92  
    93  	restart, e := client.AddOrUpdateIDPConfig(globalContext, madmin.OpenidIDPCfg, cfgName, inputCfg, update)
    94  	fatalIf(probe.NewError(e), "Unable to add OpenID IDP config to server")
    95  
    96  	// Print set config result
    97  	printMsg(configSetMessage{
    98  		targetAlias: aliasedURL,
    99  		restart:     restart,
   100  	})
   101  
   102  	return nil
   103  }
   104  
   105  var idpOpenidUpdateCmd = cli.Command{
   106  	Name:         "update",
   107  	Usage:        "Update an OpenID IDP configuration",
   108  	Action:       mainIDPOpenIDUpdate,
   109  	Before:       setGlobalsFromContext,
   110  	OnUsageError: onUsageError,
   111  	Flags:        globalFlags,
   112  	CustomHelpTemplate: `NAME:
   113    {{.HelpName}} - {{.Usage}}
   114  
   115  USAGE:
   116    {{.HelpName}} TARGET [CFG_NAME] [CFG_PARAMS...]
   117  
   118  FLAGS:
   119    {{range .VisibleFlags}}{{.}}
   120    {{end}}
   121  EXAMPLES:
   122    1. Update the default OpenID IDP configuration (CFG_NAME is omitted).
   123       {{.Prompt}} {{.HelpName}} play/
   124            scopes="openid,groups" \
   125            role_policy="consoleAdmin"
   126    2. Update configuration for OpenID IDP configuration named "dex_test".
   127       {{.Prompt}} {{.HelpName}} play/ dex_test \
   128            scopes="openid,groups" \
   129            role_policy="consoleAdmin"
   130  `,
   131  }
   132  
   133  func mainIDPOpenIDUpdate(ctx *cli.Context) error {
   134  	return mainIDPOpenIDAddOrUpdate(ctx, true)
   135  }
   136  
   137  var idpOpenidRemoveCmd = cli.Command{
   138  	Name:         "remove",
   139  	ShortName:    "rm",
   140  	Usage:        "remove OpenID IDP server configuration",
   141  	Action:       mainIDPOpenIDRemove,
   142  	Before:       setGlobalsFromContext,
   143  	Flags:        globalFlags,
   144  	OnUsageError: onUsageError,
   145  	CustomHelpTemplate: `NAME:
   146    {{.HelpName}} - {{.Usage}}
   147  
   148  USAGE:
   149    {{.HelpName}} TARGET [CFG_NAME]
   150  
   151  FLAGS:
   152    {{range .VisibleFlags}}{{.}}
   153    {{end}}
   154  EXAMPLES:
   155    1. Remove the default OpenID IDP configuration (CFG_NAME is omitted).
   156       {{.Prompt}} {{.HelpName}} play/
   157    2. Remove OpenID IDP configuration named "dex_test".
   158       {{.Prompt}} {{.HelpName}} play/ dex_test
   159  `,
   160  }
   161  
   162  func mainIDPOpenIDRemove(ctx *cli.Context) error {
   163  	if len(ctx.Args()) < 1 || len(ctx.Args()) > 2 {
   164  		showCommandHelpAndExit(ctx, 1)
   165  	}
   166  
   167  	args := ctx.Args()
   168  
   169  	var cfgName string
   170  	if len(args) == 2 {
   171  		cfgName = args.Get(1)
   172  	}
   173  	return idpRemove(ctx, true, cfgName)
   174  }
   175  
   176  func idpRemove(ctx *cli.Context, isOpenID bool, cfgName string) error {
   177  	args := ctx.Args()
   178  	aliasedURL := args.Get(0)
   179  
   180  	// Create a new MinIO Admin Client
   181  	client, err := newAdminClient(aliasedURL)
   182  	fatalIf(err, "Unable to initialize admin connection.")
   183  
   184  	idpType := madmin.LDAPIDPCfg
   185  	if isOpenID {
   186  		idpType = madmin.OpenidIDPCfg
   187  	}
   188  
   189  	restart, e := client.DeleteIDPConfig(globalContext, idpType, cfgName)
   190  	fatalIf(probe.NewError(e), "Unable to remove %s IDP config '%s'", idpType, cfgName)
   191  
   192  	printMsg(configSetMessage{
   193  		targetAlias: aliasedURL,
   194  		restart:     restart,
   195  	})
   196  
   197  	return nil
   198  }
   199  
   200  var idpOpenidListCmd = cli.Command{
   201  	Name:         "list",
   202  	ShortName:    "ls",
   203  	Usage:        "list OpenID IDP server configuration(s)",
   204  	Action:       mainIDPOpenIDList,
   205  	Before:       setGlobalsFromContext,
   206  	Flags:        globalFlags,
   207  	OnUsageError: onUsageError,
   208  	CustomHelpTemplate: `NAME:
   209    {{.HelpName}} - {{.Usage}}
   210  
   211  USAGE:
   212    {{.HelpName}} TARGET
   213  
   214  FLAGS:
   215    {{range .VisibleFlags}}{{.}}
   216    {{end}}
   217  EXAMPLES:
   218    1. List configurations for OpenID IDP.
   219       {{.Prompt}} {{.HelpName}} play/
   220  `,
   221  }
   222  
   223  func mainIDPOpenIDList(ctx *cli.Context) error {
   224  	if len(ctx.Args()) != 1 {
   225  		showCommandHelpAndExit(ctx, 1)
   226  	}
   227  
   228  	return idpListCommon(ctx, true)
   229  }
   230  
   231  func idpListCommon(ctx *cli.Context, isOpenID bool) error {
   232  	args := ctx.Args()
   233  	aliasedURL := args.Get(0)
   234  
   235  	// Create a new MinIO Admin Client
   236  	client, err := newAdminClient(aliasedURL)
   237  	fatalIf(err, "Unable to initialize admin connection.")
   238  
   239  	idpType := madmin.LDAPIDPCfg
   240  	if isOpenID {
   241  		idpType = madmin.OpenidIDPCfg
   242  	}
   243  	result, e := client.ListIDPConfig(globalContext, idpType)
   244  	fatalIf(probe.NewError(e), "Unable to list IDP config for '%s'", idpType)
   245  
   246  	printMsg(idpCfgList(result))
   247  
   248  	return nil
   249  }
   250  
   251  type idpCfgList []madmin.IDPListItem
   252  
   253  func (i idpCfgList) JSON() string {
   254  	bs, e := json.MarshalIndent(i, "", "  ")
   255  	fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
   256  
   257  	return string(bs)
   258  }
   259  
   260  func (i idpCfgList) String() string {
   261  	maxNameWidth := len("Name")
   262  	maxRoleARNWidth := len("RoleArn")
   263  	for _, item := range i {
   264  		name := item.Name
   265  		if name == "_" {
   266  			name = "(default)" // for the un-named config, don't show `_`
   267  		}
   268  		if maxNameWidth < len(name) {
   269  			maxNameWidth = len(name)
   270  		}
   271  		if maxRoleARNWidth < len(item.RoleARN) {
   272  			maxRoleARNWidth = len(item.RoleARN)
   273  		}
   274  	}
   275  	enabledWidth := 5
   276  	// Add 2 for padding
   277  	maxNameWidth += 2
   278  	maxRoleARNWidth += 2
   279  
   280  	enabledColStyle := lipgloss.NewStyle().
   281  		Align(lipgloss.Center).
   282  		PaddingLeft(1).
   283  		PaddingRight(1).
   284  		Width(enabledWidth)
   285  	nameColStyle := lipgloss.NewStyle().
   286  		Align(lipgloss.Right).
   287  		PaddingLeft(1).
   288  		PaddingRight(1).
   289  		Width(maxNameWidth)
   290  	arnColStyle := lipgloss.NewStyle().
   291  		Align(lipgloss.Left).
   292  		PaddingLeft(1).
   293  		PaddingRight(1).
   294  		Foreground(lipgloss.Color("#04B575")). // green
   295  		Width(maxRoleARNWidth)
   296  
   297  	styles := []lipgloss.Style{enabledColStyle, nameColStyle, arnColStyle}
   298  
   299  	headers := []string{"On?", "Name", "RoleARN"}
   300  	headerRow := []string{}
   301  
   302  	// Override some style settings for the header
   303  	for ii, hdr := range headers {
   304  		headerRow = append(headerRow,
   305  			styles[ii].Copy().
   306  				Bold(true).
   307  				Foreground(lipgloss.Color("#6495ed")). // green
   308  				Align(lipgloss.Center).
   309  				Render(hdr),
   310  		)
   311  	}
   312  
   313  	lines := []string{strings.Join(headerRow, "")}
   314  
   315  	enabledOff := "🔴"
   316  	enabledOn := "🟢"
   317  
   318  	for _, item := range i {
   319  		enabled := enabledOff
   320  		if item.Enabled {
   321  			enabled = enabledOn
   322  		}
   323  
   324  		line := []string{
   325  			styles[0].Render(enabled),
   326  			styles[1].Render(item.Name),
   327  			styles[2].Render(item.RoleARN),
   328  		}
   329  		if item.Name == "_" {
   330  			// For default config, don't display `_` and make it look faint.
   331  			line[1] = styles[1].Copy().
   332  				Faint(true).
   333  				Render("(default)")
   334  		}
   335  		lines = append(lines, strings.Join(line, ""))
   336  	}
   337  
   338  	boxContent := strings.Join(lines, "\n")
   339  	boxStyle := lipgloss.NewStyle().
   340  		BorderStyle(lipgloss.RoundedBorder())
   341  
   342  	return boxStyle.Render(boxContent)
   343  }
   344  
   345  var idpOpenidInfoCmd = cli.Command{
   346  	Name:         "info",
   347  	Usage:        "get OpenID IDP server configuration info",
   348  	Action:       mainIDPOpenIDInfo,
   349  	Before:       setGlobalsFromContext,
   350  	Flags:        globalFlags,
   351  	OnUsageError: onUsageError,
   352  	CustomHelpTemplate: `NAME:
   353    {{.HelpName}} - {{.Usage}}
   354  
   355  USAGE:
   356    {{.HelpName}} TARGET [CFG_NAME]
   357  
   358  FLAGS:
   359    {{range .VisibleFlags}}{{.}}
   360    {{end}}
   361  EXAMPLES:
   362    1. Get configuration info on the default OpenID IDP configuration (CFG_NAME is omitted).
   363       {{.Prompt}} {{.HelpName}} play/
   364    2. Get configuration info on OpenID IDP configuration named "dex_test".
   365       {{.Prompt}} {{.HelpName}} play/ dex_test
   366  `,
   367  }
   368  
   369  func mainIDPOpenIDInfo(ctx *cli.Context) error {
   370  	if len(ctx.Args()) < 1 || len(ctx.Args()) > 2 {
   371  		showCommandHelpAndExit(ctx, 1)
   372  	}
   373  
   374  	args := ctx.Args()
   375  	var cfgName string
   376  	if len(args) == 2 {
   377  		cfgName = args.Get(1)
   378  	}
   379  
   380  	return idpInfo(ctx, true, cfgName)
   381  }
   382  
   383  func idpInfo(ctx *cli.Context, isOpenID bool, cfgName string) error {
   384  	args := ctx.Args()
   385  	aliasedURL := args.Get(0)
   386  
   387  	// Create a new MinIO Admin Client
   388  	client, err := newAdminClient(aliasedURL)
   389  	fatalIf(err, "Unable to initialize admin connection.")
   390  
   391  	idpType := madmin.LDAPIDPCfg
   392  	if isOpenID {
   393  		idpType = madmin.OpenidIDPCfg
   394  	}
   395  
   396  	result, e := client.GetIDPConfig(globalContext, idpType, cfgName)
   397  	fatalIf(probe.NewError(e), "Unable to get %s IDP config from server", idpType)
   398  
   399  	// Print set config result
   400  	printMsg(idpConfig(result))
   401  
   402  	return nil
   403  }
   404  
   405  type idpConfig madmin.IDPConfig
   406  
   407  func (i idpConfig) JSON() string {
   408  	bs, e := json.MarshalIndent(i, "", "  ")
   409  	fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
   410  
   411  	return string(bs)
   412  }
   413  
   414  func (i idpConfig) String() string {
   415  	if len(i.Info) == 0 {
   416  		return "Not configured."
   417  	}
   418  
   419  	// Determine required width for key column.
   420  	fieldColWidth := 0
   421  	for _, kv := range i.Info {
   422  		if fieldColWidth < len(kv.Key) {
   423  			fieldColWidth = len(kv.Key)
   424  		}
   425  	}
   426  	// Add 1 for the colon-suffix in each entry.
   427  	fieldColWidth++
   428  
   429  	fieldColStyle := lipgloss.NewStyle().
   430  		Width(fieldColWidth).
   431  		Foreground(lipgloss.Color("#04B575")). // green
   432  		Bold(true).
   433  		Align(lipgloss.Right)
   434  	valueColStyle := lipgloss.NewStyle().
   435  		PaddingLeft(1).
   436  		Align(lipgloss.Left)
   437  	envMarkStyle := lipgloss.NewStyle().
   438  		Foreground(lipgloss.Color("201")). // pinkish-red
   439  		PaddingLeft(1)
   440  
   441  	var lines []string
   442  	for _, kv := range i.Info {
   443  		envStr := ""
   444  		if kv.IsCfg && kv.IsEnv {
   445  			envStr = " (environment)"
   446  		}
   447  		lines = append(lines, fmt.Sprintf("%s%s%s",
   448  			fieldColStyle.Render(kv.Key+":"),
   449  			valueColStyle.Render(kv.Value),
   450  			envMarkStyle.Render(envStr),
   451  		))
   452  	}
   453  
   454  	boxContent := strings.Join(lines, "\n")
   455  
   456  	boxStyle := lipgloss.NewStyle().
   457  		BorderStyle(lipgloss.RoundedBorder())
   458  
   459  	return boxStyle.Render(boxContent)
   460  }
   461  
   462  var idpOpenidEnableCmd = cli.Command{
   463  	Name:         "enable",
   464  	Usage:        "enable an OpenID IDP server configuration",
   465  	Action:       mainIDPOpenIDEnable,
   466  	Before:       setGlobalsFromContext,
   467  	Flags:        globalFlags,
   468  	OnUsageError: onUsageError,
   469  	CustomHelpTemplate: `NAME:
   470    {{.HelpName}} - {{.Usage}}
   471  
   472  USAGE:
   473    {{.HelpName}} TARGET [CFG_NAME]
   474  
   475  FLAGS:
   476    {{range .VisibleFlags}}{{.}}
   477    {{end}}
   478  EXAMPLES:
   479    1. Enable the default OpenID IDP configuration (CFG_NAME is omitted).
   480       {{.Prompt}} {{.HelpName}} play/
   481    2. Enable OpenID IDP configuration named "dex_test".
   482       {{.Prompt}} {{.HelpName}} play/ dex_test
   483  `,
   484  }
   485  
   486  func mainIDPOpenIDEnable(ctx *cli.Context) error {
   487  	isOpenID, enable := true, true
   488  	return idpEnableDisable(ctx, isOpenID, enable)
   489  }
   490  
   491  func idpEnableDisable(ctx *cli.Context, isOpenID, enable bool) error {
   492  	if len(ctx.Args()) < 1 || len(ctx.Args()) > 2 {
   493  		showCommandHelpAndExit(ctx, 1)
   494  	}
   495  
   496  	args := ctx.Args()
   497  	cfgName := madmin.Default
   498  	if len(args) == 2 {
   499  		cfgName = args.Get(1)
   500  	}
   501  	aliasedURL := args.Get(0)
   502  
   503  	// Create a new MinIO Admin Client
   504  	client, err := newAdminClient(aliasedURL)
   505  	fatalIf(err, "Unable to initialize admin connection.")
   506  
   507  	idpType := madmin.LDAPIDPCfg
   508  	if isOpenID {
   509  		idpType = madmin.OpenidIDPCfg
   510  	}
   511  
   512  	configBody := "enable=on"
   513  	if !enable {
   514  		configBody = "enable=off"
   515  	}
   516  
   517  	restart, e := client.AddOrUpdateIDPConfig(globalContext, idpType, cfgName, configBody, true)
   518  	fatalIf(probe.NewError(e), "Unable to remove %s IDP config '%s'", idpType, cfgName)
   519  
   520  	printMsg(configSetMessage{
   521  		targetAlias: aliasedURL,
   522  		restart:     restart,
   523  	})
   524  
   525  	return nil
   526  }
   527  
   528  var idpOpenidDisableCmd = cli.Command{
   529  	Name:         "disable",
   530  	Usage:        "Disable an OpenID IDP server configuration",
   531  	Action:       mainIDPOpenIDDisable,
   532  	Before:       setGlobalsFromContext,
   533  	Flags:        globalFlags,
   534  	OnUsageError: onUsageError,
   535  	CustomHelpTemplate: `NAME:
   536    {{.HelpName}} - {{.Usage}}
   537  
   538  USAGE:
   539    {{.HelpName}} TARGET [CFG_NAME]
   540  
   541  FLAGS:
   542    {{range .VisibleFlags}}{{.}}
   543    {{end}}
   544  EXAMPLES:
   545    1. Disable the default OpenID IDP configuration (CFG_NAME is omitted).
   546       {{.Prompt}} {{.HelpName}} play/
   547    2. Disable OpenID IDP configuration named "dex_test".
   548       {{.Prompt}} {{.HelpName}} play/ dex_test
   549  `,
   550  }
   551  
   552  func mainIDPOpenIDDisable(ctx *cli.Context) error {
   553  	isOpenID, enable := true, false
   554  	return idpEnableDisable(ctx, isOpenID, enable)
   555  }