code.gitea.io/gitea@v1.21.7/cmd/admin.go (about)

     1  // Copyright 2016 The Gogs Authors. All rights reserved.
     2  // Copyright 2016 The Gitea Authors. All rights reserved.
     3  // SPDX-License-Identifier: MIT
     4  
     5  package cmd
     6  
     7  import (
     8  	"context"
     9  	"errors"
    10  	"fmt"
    11  	"net/url"
    12  	"os"
    13  	"strings"
    14  	"text/tabwriter"
    15  
    16  	asymkey_model "code.gitea.io/gitea/models/asymkey"
    17  	auth_model "code.gitea.io/gitea/models/auth"
    18  	"code.gitea.io/gitea/models/db"
    19  	repo_model "code.gitea.io/gitea/models/repo"
    20  	"code.gitea.io/gitea/modules/git"
    21  	"code.gitea.io/gitea/modules/graceful"
    22  	"code.gitea.io/gitea/modules/log"
    23  	repo_module "code.gitea.io/gitea/modules/repository"
    24  	"code.gitea.io/gitea/modules/util"
    25  	auth_service "code.gitea.io/gitea/services/auth"
    26  	"code.gitea.io/gitea/services/auth/source/oauth2"
    27  	"code.gitea.io/gitea/services/auth/source/smtp"
    28  	repo_service "code.gitea.io/gitea/services/repository"
    29  
    30  	"github.com/urfave/cli/v2"
    31  )
    32  
    33  var (
    34  	// CmdAdmin represents the available admin sub-command.
    35  	CmdAdmin = &cli.Command{
    36  		Name:  "admin",
    37  		Usage: "Command line interface to perform common administrative operations",
    38  		Subcommands: []*cli.Command{
    39  			subcmdUser,
    40  			subcmdRepoSyncReleases,
    41  			subcmdRegenerate,
    42  			subcmdAuth,
    43  			subcmdSendMail,
    44  		},
    45  	}
    46  
    47  	subcmdRepoSyncReleases = &cli.Command{
    48  		Name:   "repo-sync-releases",
    49  		Usage:  "Synchronize repository releases with tags",
    50  		Action: runRepoSyncReleases,
    51  	}
    52  
    53  	subcmdRegenerate = &cli.Command{
    54  		Name:  "regenerate",
    55  		Usage: "Regenerate specific files",
    56  		Subcommands: []*cli.Command{
    57  			microcmdRegenHooks,
    58  			microcmdRegenKeys,
    59  		},
    60  	}
    61  
    62  	microcmdRegenHooks = &cli.Command{
    63  		Name:   "hooks",
    64  		Usage:  "Regenerate git-hooks",
    65  		Action: runRegenerateHooks,
    66  	}
    67  
    68  	microcmdRegenKeys = &cli.Command{
    69  		Name:   "keys",
    70  		Usage:  "Regenerate authorized_keys file",
    71  		Action: runRegenerateKeys,
    72  	}
    73  
    74  	subcmdAuth = &cli.Command{
    75  		Name:  "auth",
    76  		Usage: "Modify external auth providers",
    77  		Subcommands: []*cli.Command{
    78  			microcmdAuthAddOauth,
    79  			microcmdAuthUpdateOauth,
    80  			cmdAuthAddLdapBindDn,
    81  			cmdAuthUpdateLdapBindDn,
    82  			cmdAuthAddLdapSimpleAuth,
    83  			cmdAuthUpdateLdapSimpleAuth,
    84  			microcmdAuthAddSMTP,
    85  			microcmdAuthUpdateSMTP,
    86  			microcmdAuthList,
    87  			microcmdAuthDelete,
    88  		},
    89  	}
    90  
    91  	microcmdAuthList = &cli.Command{
    92  		Name:   "list",
    93  		Usage:  "List auth sources",
    94  		Action: runListAuth,
    95  		Flags: []cli.Flag{
    96  			&cli.IntFlag{
    97  				Name:  "min-width",
    98  				Usage: "Minimal cell width including any padding for the formatted table",
    99  				Value: 0,
   100  			},
   101  			&cli.IntFlag{
   102  				Name:  "tab-width",
   103  				Usage: "width of tab characters in formatted table (equivalent number of spaces)",
   104  				Value: 8,
   105  			},
   106  			&cli.IntFlag{
   107  				Name:  "padding",
   108  				Usage: "padding added to a cell before computing its width",
   109  				Value: 1,
   110  			},
   111  			&cli.StringFlag{
   112  				Name:  "pad-char",
   113  				Usage: `ASCII char used for padding if padchar == '\\t', the Writer will assume that the width of a '\\t' in the formatted output is tabwidth, and cells are left-aligned independent of align_left (for correct-looking results, tabwidth must correspond to the tab width in the viewer displaying the result)`,
   114  				Value: "\t",
   115  			},
   116  			&cli.BoolFlag{
   117  				Name:  "vertical-bars",
   118  				Usage: "Set to true to print vertical bars between columns",
   119  			},
   120  		},
   121  	}
   122  
   123  	idFlag = &cli.Int64Flag{
   124  		Name:  "id",
   125  		Usage: "ID of authentication source",
   126  	}
   127  
   128  	microcmdAuthDelete = &cli.Command{
   129  		Name:   "delete",
   130  		Usage:  "Delete specific auth source",
   131  		Flags:  []cli.Flag{idFlag},
   132  		Action: runDeleteAuth,
   133  	}
   134  
   135  	oauthCLIFlags = []cli.Flag{
   136  		&cli.StringFlag{
   137  			Name:  "name",
   138  			Value: "",
   139  			Usage: "Application Name",
   140  		},
   141  		&cli.StringFlag{
   142  			Name:  "provider",
   143  			Value: "",
   144  			Usage: "OAuth2 Provider",
   145  		},
   146  		&cli.StringFlag{
   147  			Name:  "key",
   148  			Value: "",
   149  			Usage: "Client ID (Key)",
   150  		},
   151  		&cli.StringFlag{
   152  			Name:  "secret",
   153  			Value: "",
   154  			Usage: "Client Secret",
   155  		},
   156  		&cli.StringFlag{
   157  			Name:  "auto-discover-url",
   158  			Value: "",
   159  			Usage: "OpenID Connect Auto Discovery URL (only required when using OpenID Connect as provider)",
   160  		},
   161  		&cli.StringFlag{
   162  			Name:  "use-custom-urls",
   163  			Value: "false",
   164  			Usage: "Use custom URLs for GitLab/GitHub OAuth endpoints",
   165  		},
   166  		&cli.StringFlag{
   167  			Name:  "custom-tenant-id",
   168  			Value: "",
   169  			Usage: "Use custom Tenant ID for OAuth endpoints",
   170  		},
   171  		&cli.StringFlag{
   172  			Name:  "custom-auth-url",
   173  			Value: "",
   174  			Usage: "Use a custom Authorization URL (option for GitLab/GitHub)",
   175  		},
   176  		&cli.StringFlag{
   177  			Name:  "custom-token-url",
   178  			Value: "",
   179  			Usage: "Use a custom Token URL (option for GitLab/GitHub)",
   180  		},
   181  		&cli.StringFlag{
   182  			Name:  "custom-profile-url",
   183  			Value: "",
   184  			Usage: "Use a custom Profile URL (option for GitLab/GitHub)",
   185  		},
   186  		&cli.StringFlag{
   187  			Name:  "custom-email-url",
   188  			Value: "",
   189  			Usage: "Use a custom Email URL (option for GitHub)",
   190  		},
   191  		&cli.StringFlag{
   192  			Name:  "icon-url",
   193  			Value: "",
   194  			Usage: "Custom icon URL for OAuth2 login source",
   195  		},
   196  		&cli.BoolFlag{
   197  			Name:  "skip-local-2fa",
   198  			Usage: "Set to true to skip local 2fa for users authenticated by this source",
   199  		},
   200  		&cli.StringSliceFlag{
   201  			Name:  "scopes",
   202  			Value: nil,
   203  			Usage: "Scopes to request when to authenticate against this OAuth2 source",
   204  		},
   205  		&cli.StringFlag{
   206  			Name:  "required-claim-name",
   207  			Value: "",
   208  			Usage: "Claim name that has to be set to allow users to login with this source",
   209  		},
   210  		&cli.StringFlag{
   211  			Name:  "required-claim-value",
   212  			Value: "",
   213  			Usage: "Claim value that has to be set to allow users to login with this source",
   214  		},
   215  		&cli.StringFlag{
   216  			Name:  "group-claim-name",
   217  			Value: "",
   218  			Usage: "Claim name providing group names for this source",
   219  		},
   220  		&cli.StringFlag{
   221  			Name:  "admin-group",
   222  			Value: "",
   223  			Usage: "Group Claim value for administrator users",
   224  		},
   225  		&cli.StringFlag{
   226  			Name:  "restricted-group",
   227  			Value: "",
   228  			Usage: "Group Claim value for restricted users",
   229  		},
   230  		&cli.StringFlag{
   231  			Name:  "group-team-map",
   232  			Value: "",
   233  			Usage: "JSON mapping between groups and org teams",
   234  		},
   235  		&cli.BoolFlag{
   236  			Name:  "group-team-map-removal",
   237  			Usage: "Activate automatic team membership removal depending on groups",
   238  		},
   239  	}
   240  
   241  	microcmdAuthUpdateOauth = &cli.Command{
   242  		Name:   "update-oauth",
   243  		Usage:  "Update existing Oauth authentication source",
   244  		Action: runUpdateOauth,
   245  		Flags:  append(oauthCLIFlags[:1], append([]cli.Flag{idFlag}, oauthCLIFlags[1:]...)...),
   246  	}
   247  
   248  	microcmdAuthAddOauth = &cli.Command{
   249  		Name:   "add-oauth",
   250  		Usage:  "Add new Oauth authentication source",
   251  		Action: runAddOauth,
   252  		Flags:  oauthCLIFlags,
   253  	}
   254  
   255  	subcmdSendMail = &cli.Command{
   256  		Name:   "sendmail",
   257  		Usage:  "Send a message to all users",
   258  		Action: runSendMail,
   259  		Flags: []cli.Flag{
   260  			&cli.StringFlag{
   261  				Name:  "title",
   262  				Usage: `a title of a message`,
   263  				Value: "",
   264  			},
   265  			&cli.StringFlag{
   266  				Name:  "content",
   267  				Usage: "a content of a message",
   268  				Value: "",
   269  			},
   270  			&cli.BoolFlag{
   271  				Name:    "force",
   272  				Aliases: []string{"f"},
   273  				Usage:   "A flag to bypass a confirmation step",
   274  			},
   275  		},
   276  	}
   277  
   278  	smtpCLIFlags = []cli.Flag{
   279  		&cli.StringFlag{
   280  			Name:  "name",
   281  			Value: "",
   282  			Usage: "Application Name",
   283  		},
   284  		&cli.StringFlag{
   285  			Name:  "auth-type",
   286  			Value: "PLAIN",
   287  			Usage: "SMTP Authentication Type (PLAIN/LOGIN/CRAM-MD5) default PLAIN",
   288  		},
   289  		&cli.StringFlag{
   290  			Name:  "host",
   291  			Value: "",
   292  			Usage: "SMTP Host",
   293  		},
   294  		&cli.IntFlag{
   295  			Name:  "port",
   296  			Usage: "SMTP Port",
   297  		},
   298  		&cli.BoolFlag{
   299  			Name:  "force-smtps",
   300  			Usage: "SMTPS is always used on port 465. Set this to force SMTPS on other ports.",
   301  			Value: true,
   302  		},
   303  		&cli.BoolFlag{
   304  			Name:  "skip-verify",
   305  			Usage: "Skip TLS verify.",
   306  			Value: true,
   307  		},
   308  		&cli.StringFlag{
   309  			Name:  "helo-hostname",
   310  			Value: "",
   311  			Usage: "Hostname sent with HELO. Leave blank to send current hostname",
   312  		},
   313  		&cli.BoolFlag{
   314  			Name:  "disable-helo",
   315  			Usage: "Disable SMTP helo.",
   316  			Value: true,
   317  		},
   318  		&cli.StringFlag{
   319  			Name:  "allowed-domains",
   320  			Value: "",
   321  			Usage: "Leave empty to allow all domains. Separate multiple domains with a comma (',')",
   322  		},
   323  		&cli.BoolFlag{
   324  			Name:  "skip-local-2fa",
   325  			Usage: "Skip 2FA to log on.",
   326  			Value: true,
   327  		},
   328  		&cli.BoolFlag{
   329  			Name:  "active",
   330  			Usage: "This Authentication Source is Activated.",
   331  			Value: true,
   332  		},
   333  	}
   334  
   335  	microcmdAuthAddSMTP = &cli.Command{
   336  		Name:   "add-smtp",
   337  		Usage:  "Add new SMTP authentication source",
   338  		Action: runAddSMTP,
   339  		Flags:  smtpCLIFlags,
   340  	}
   341  
   342  	microcmdAuthUpdateSMTP = &cli.Command{
   343  		Name:   "update-smtp",
   344  		Usage:  "Update existing SMTP authentication source",
   345  		Action: runUpdateSMTP,
   346  		Flags:  append(smtpCLIFlags[:1], append([]cli.Flag{idFlag}, smtpCLIFlags[1:]...)...),
   347  	}
   348  )
   349  
   350  func runRepoSyncReleases(_ *cli.Context) error {
   351  	ctx, cancel := installSignals()
   352  	defer cancel()
   353  
   354  	if err := initDB(ctx); err != nil {
   355  		return err
   356  	}
   357  
   358  	if err := git.InitSimple(ctx); err != nil {
   359  		return err
   360  	}
   361  
   362  	log.Trace("Synchronizing repository releases (this may take a while)")
   363  	for page := 1; ; page++ {
   364  		repos, count, err := repo_model.SearchRepositoryByName(ctx, &repo_model.SearchRepoOptions{
   365  			ListOptions: db.ListOptions{
   366  				PageSize: repo_model.RepositoryListDefaultPageSize,
   367  				Page:     page,
   368  			},
   369  			Private: true,
   370  		})
   371  		if err != nil {
   372  			return fmt.Errorf("SearchRepositoryByName: %w", err)
   373  		}
   374  		if len(repos) == 0 {
   375  			break
   376  		}
   377  		log.Trace("Processing next %d repos of %d", len(repos), count)
   378  		for _, repo := range repos {
   379  			log.Trace("Synchronizing repo %s with path %s", repo.FullName(), repo.RepoPath())
   380  			gitRepo, err := git.OpenRepository(ctx, repo.RepoPath())
   381  			if err != nil {
   382  				log.Warn("OpenRepository: %v", err)
   383  				continue
   384  			}
   385  
   386  			oldnum, err := getReleaseCount(ctx, repo.ID)
   387  			if err != nil {
   388  				log.Warn(" GetReleaseCountByRepoID: %v", err)
   389  			}
   390  			log.Trace(" currentNumReleases is %d, running SyncReleasesWithTags", oldnum)
   391  
   392  			if err = repo_module.SyncReleasesWithTags(ctx, repo, gitRepo); err != nil {
   393  				log.Warn(" SyncReleasesWithTags: %v", err)
   394  				gitRepo.Close()
   395  				continue
   396  			}
   397  
   398  			count, err = getReleaseCount(ctx, repo.ID)
   399  			if err != nil {
   400  				log.Warn(" GetReleaseCountByRepoID: %v", err)
   401  				gitRepo.Close()
   402  				continue
   403  			}
   404  
   405  			log.Trace(" repo %s releases synchronized to tags: from %d to %d",
   406  				repo.FullName(), oldnum, count)
   407  			gitRepo.Close()
   408  		}
   409  	}
   410  
   411  	return nil
   412  }
   413  
   414  func getReleaseCount(ctx context.Context, id int64) (int64, error) {
   415  	return repo_model.GetReleaseCountByRepoID(
   416  		ctx,
   417  		id,
   418  		repo_model.FindReleasesOptions{
   419  			IncludeTags: true,
   420  		},
   421  	)
   422  }
   423  
   424  func runRegenerateHooks(_ *cli.Context) error {
   425  	ctx, cancel := installSignals()
   426  	defer cancel()
   427  
   428  	if err := initDB(ctx); err != nil {
   429  		return err
   430  	}
   431  	return repo_service.SyncRepositoryHooks(graceful.GetManager().ShutdownContext())
   432  }
   433  
   434  func runRegenerateKeys(_ *cli.Context) error {
   435  	ctx, cancel := installSignals()
   436  	defer cancel()
   437  
   438  	if err := initDB(ctx); err != nil {
   439  		return err
   440  	}
   441  	return asymkey_model.RewriteAllPublicKeys(ctx)
   442  }
   443  
   444  func parseOAuth2Config(c *cli.Context) *oauth2.Source {
   445  	var customURLMapping *oauth2.CustomURLMapping
   446  	if c.IsSet("use-custom-urls") {
   447  		customURLMapping = &oauth2.CustomURLMapping{
   448  			TokenURL:   c.String("custom-token-url"),
   449  			AuthURL:    c.String("custom-auth-url"),
   450  			ProfileURL: c.String("custom-profile-url"),
   451  			EmailURL:   c.String("custom-email-url"),
   452  			Tenant:     c.String("custom-tenant-id"),
   453  		}
   454  	} else {
   455  		customURLMapping = nil
   456  	}
   457  	return &oauth2.Source{
   458  		Provider:                      c.String("provider"),
   459  		ClientID:                      c.String("key"),
   460  		ClientSecret:                  c.String("secret"),
   461  		OpenIDConnectAutoDiscoveryURL: c.String("auto-discover-url"),
   462  		CustomURLMapping:              customURLMapping,
   463  		IconURL:                       c.String("icon-url"),
   464  		SkipLocalTwoFA:                c.Bool("skip-local-2fa"),
   465  		Scopes:                        c.StringSlice("scopes"),
   466  		RequiredClaimName:             c.String("required-claim-name"),
   467  		RequiredClaimValue:            c.String("required-claim-value"),
   468  		GroupClaimName:                c.String("group-claim-name"),
   469  		AdminGroup:                    c.String("admin-group"),
   470  		RestrictedGroup:               c.String("restricted-group"),
   471  		GroupTeamMap:                  c.String("group-team-map"),
   472  		GroupTeamMapRemoval:           c.Bool("group-team-map-removal"),
   473  	}
   474  }
   475  
   476  func runAddOauth(c *cli.Context) error {
   477  	ctx, cancel := installSignals()
   478  	defer cancel()
   479  
   480  	if err := initDB(ctx); err != nil {
   481  		return err
   482  	}
   483  
   484  	config := parseOAuth2Config(c)
   485  	if config.Provider == "openidConnect" {
   486  		discoveryURL, err := url.Parse(config.OpenIDConnectAutoDiscoveryURL)
   487  		if err != nil || (discoveryURL.Scheme != "http" && discoveryURL.Scheme != "https") {
   488  			return fmt.Errorf("invalid Auto Discovery URL: %s (this must be a valid URL starting with http:// or https://)", config.OpenIDConnectAutoDiscoveryURL)
   489  		}
   490  	}
   491  
   492  	return auth_model.CreateSource(&auth_model.Source{
   493  		Type:     auth_model.OAuth2,
   494  		Name:     c.String("name"),
   495  		IsActive: true,
   496  		Cfg:      config,
   497  	})
   498  }
   499  
   500  func runUpdateOauth(c *cli.Context) error {
   501  	if !c.IsSet("id") {
   502  		return fmt.Errorf("--id flag is missing")
   503  	}
   504  
   505  	ctx, cancel := installSignals()
   506  	defer cancel()
   507  
   508  	if err := initDB(ctx); err != nil {
   509  		return err
   510  	}
   511  
   512  	source, err := auth_model.GetSourceByID(c.Int64("id"))
   513  	if err != nil {
   514  		return err
   515  	}
   516  
   517  	oAuth2Config := source.Cfg.(*oauth2.Source)
   518  
   519  	if c.IsSet("name") {
   520  		source.Name = c.String("name")
   521  	}
   522  
   523  	if c.IsSet("provider") {
   524  		oAuth2Config.Provider = c.String("provider")
   525  	}
   526  
   527  	if c.IsSet("key") {
   528  		oAuth2Config.ClientID = c.String("key")
   529  	}
   530  
   531  	if c.IsSet("secret") {
   532  		oAuth2Config.ClientSecret = c.String("secret")
   533  	}
   534  
   535  	if c.IsSet("auto-discover-url") {
   536  		oAuth2Config.OpenIDConnectAutoDiscoveryURL = c.String("auto-discover-url")
   537  	}
   538  
   539  	if c.IsSet("icon-url") {
   540  		oAuth2Config.IconURL = c.String("icon-url")
   541  	}
   542  
   543  	if c.IsSet("scopes") {
   544  		oAuth2Config.Scopes = c.StringSlice("scopes")
   545  	}
   546  
   547  	if c.IsSet("required-claim-name") {
   548  		oAuth2Config.RequiredClaimName = c.String("required-claim-name")
   549  	}
   550  	if c.IsSet("required-claim-value") {
   551  		oAuth2Config.RequiredClaimValue = c.String("required-claim-value")
   552  	}
   553  
   554  	if c.IsSet("group-claim-name") {
   555  		oAuth2Config.GroupClaimName = c.String("group-claim-name")
   556  	}
   557  	if c.IsSet("admin-group") {
   558  		oAuth2Config.AdminGroup = c.String("admin-group")
   559  	}
   560  	if c.IsSet("restricted-group") {
   561  		oAuth2Config.RestrictedGroup = c.String("restricted-group")
   562  	}
   563  	if c.IsSet("group-team-map") {
   564  		oAuth2Config.GroupTeamMap = c.String("group-team-map")
   565  	}
   566  	if c.IsSet("group-team-map-removal") {
   567  		oAuth2Config.GroupTeamMapRemoval = c.Bool("group-team-map-removal")
   568  	}
   569  
   570  	// update custom URL mapping
   571  	customURLMapping := &oauth2.CustomURLMapping{}
   572  
   573  	if oAuth2Config.CustomURLMapping != nil {
   574  		customURLMapping.TokenURL = oAuth2Config.CustomURLMapping.TokenURL
   575  		customURLMapping.AuthURL = oAuth2Config.CustomURLMapping.AuthURL
   576  		customURLMapping.ProfileURL = oAuth2Config.CustomURLMapping.ProfileURL
   577  		customURLMapping.EmailURL = oAuth2Config.CustomURLMapping.EmailURL
   578  		customURLMapping.Tenant = oAuth2Config.CustomURLMapping.Tenant
   579  	}
   580  	if c.IsSet("use-custom-urls") && c.IsSet("custom-token-url") {
   581  		customURLMapping.TokenURL = c.String("custom-token-url")
   582  	}
   583  
   584  	if c.IsSet("use-custom-urls") && c.IsSet("custom-auth-url") {
   585  		customURLMapping.AuthURL = c.String("custom-auth-url")
   586  	}
   587  
   588  	if c.IsSet("use-custom-urls") && c.IsSet("custom-profile-url") {
   589  		customURLMapping.ProfileURL = c.String("custom-profile-url")
   590  	}
   591  
   592  	if c.IsSet("use-custom-urls") && c.IsSet("custom-email-url") {
   593  		customURLMapping.EmailURL = c.String("custom-email-url")
   594  	}
   595  
   596  	if c.IsSet("use-custom-urls") && c.IsSet("custom-tenant-id") {
   597  		customURLMapping.Tenant = c.String("custom-tenant-id")
   598  	}
   599  
   600  	oAuth2Config.CustomURLMapping = customURLMapping
   601  	source.Cfg = oAuth2Config
   602  
   603  	return auth_model.UpdateSource(source)
   604  }
   605  
   606  func parseSMTPConfig(c *cli.Context, conf *smtp.Source) error {
   607  	if c.IsSet("auth-type") {
   608  		conf.Auth = c.String("auth-type")
   609  		validAuthTypes := []string{"PLAIN", "LOGIN", "CRAM-MD5"}
   610  		if !util.SliceContainsString(validAuthTypes, strings.ToUpper(c.String("auth-type"))) {
   611  			return errors.New("Auth must be one of PLAIN/LOGIN/CRAM-MD5")
   612  		}
   613  		conf.Auth = c.String("auth-type")
   614  	}
   615  	if c.IsSet("host") {
   616  		conf.Host = c.String("host")
   617  	}
   618  	if c.IsSet("port") {
   619  		conf.Port = c.Int("port")
   620  	}
   621  	if c.IsSet("allowed-domains") {
   622  		conf.AllowedDomains = c.String("allowed-domains")
   623  	}
   624  	if c.IsSet("force-smtps") {
   625  		conf.ForceSMTPS = c.Bool("force-smtps")
   626  	}
   627  	if c.IsSet("skip-verify") {
   628  		conf.SkipVerify = c.Bool("skip-verify")
   629  	}
   630  	if c.IsSet("helo-hostname") {
   631  		conf.HeloHostname = c.String("helo-hostname")
   632  	}
   633  	if c.IsSet("disable-helo") {
   634  		conf.DisableHelo = c.Bool("disable-helo")
   635  	}
   636  	if c.IsSet("skip-local-2fa") {
   637  		conf.SkipLocalTwoFA = c.Bool("skip-local-2fa")
   638  	}
   639  	return nil
   640  }
   641  
   642  func runAddSMTP(c *cli.Context) error {
   643  	ctx, cancel := installSignals()
   644  	defer cancel()
   645  
   646  	if err := initDB(ctx); err != nil {
   647  		return err
   648  	}
   649  
   650  	if !c.IsSet("name") || len(c.String("name")) == 0 {
   651  		return errors.New("name must be set")
   652  	}
   653  	if !c.IsSet("host") || len(c.String("host")) == 0 {
   654  		return errors.New("host must be set")
   655  	}
   656  	if !c.IsSet("port") {
   657  		return errors.New("port must be set")
   658  	}
   659  	active := true
   660  	if c.IsSet("active") {
   661  		active = c.Bool("active")
   662  	}
   663  
   664  	var smtpConfig smtp.Source
   665  	if err := parseSMTPConfig(c, &smtpConfig); err != nil {
   666  		return err
   667  	}
   668  
   669  	// If not set default to PLAIN
   670  	if len(smtpConfig.Auth) == 0 {
   671  		smtpConfig.Auth = "PLAIN"
   672  	}
   673  
   674  	return auth_model.CreateSource(&auth_model.Source{
   675  		Type:     auth_model.SMTP,
   676  		Name:     c.String("name"),
   677  		IsActive: active,
   678  		Cfg:      &smtpConfig,
   679  	})
   680  }
   681  
   682  func runUpdateSMTP(c *cli.Context) error {
   683  	if !c.IsSet("id") {
   684  		return fmt.Errorf("--id flag is missing")
   685  	}
   686  
   687  	ctx, cancel := installSignals()
   688  	defer cancel()
   689  
   690  	if err := initDB(ctx); err != nil {
   691  		return err
   692  	}
   693  
   694  	source, err := auth_model.GetSourceByID(c.Int64("id"))
   695  	if err != nil {
   696  		return err
   697  	}
   698  
   699  	smtpConfig := source.Cfg.(*smtp.Source)
   700  
   701  	if err := parseSMTPConfig(c, smtpConfig); err != nil {
   702  		return err
   703  	}
   704  
   705  	if c.IsSet("name") {
   706  		source.Name = c.String("name")
   707  	}
   708  
   709  	if c.IsSet("active") {
   710  		source.IsActive = c.Bool("active")
   711  	}
   712  
   713  	source.Cfg = smtpConfig
   714  
   715  	return auth_model.UpdateSource(source)
   716  }
   717  
   718  func runListAuth(c *cli.Context) error {
   719  	ctx, cancel := installSignals()
   720  	defer cancel()
   721  
   722  	if err := initDB(ctx); err != nil {
   723  		return err
   724  	}
   725  
   726  	authSources, err := auth_model.Sources()
   727  	if err != nil {
   728  		return err
   729  	}
   730  
   731  	flags := tabwriter.AlignRight
   732  	if c.Bool("vertical-bars") {
   733  		flags |= tabwriter.Debug
   734  	}
   735  
   736  	padChar := byte('\t')
   737  	if len(c.String("pad-char")) > 0 {
   738  		padChar = c.String("pad-char")[0]
   739  	}
   740  
   741  	// loop through each source and print
   742  	w := tabwriter.NewWriter(os.Stdout, c.Int("min-width"), c.Int("tab-width"), c.Int("padding"), padChar, flags)
   743  	fmt.Fprintf(w, "ID\tName\tType\tEnabled\n")
   744  	for _, source := range authSources {
   745  		fmt.Fprintf(w, "%d\t%s\t%s\t%t\n", source.ID, source.Name, source.Type.String(), source.IsActive)
   746  	}
   747  	w.Flush()
   748  
   749  	return nil
   750  }
   751  
   752  func runDeleteAuth(c *cli.Context) error {
   753  	if !c.IsSet("id") {
   754  		return fmt.Errorf("--id flag is missing")
   755  	}
   756  
   757  	ctx, cancel := installSignals()
   758  	defer cancel()
   759  
   760  	if err := initDB(ctx); err != nil {
   761  		return err
   762  	}
   763  
   764  	source, err := auth_model.GetSourceByID(c.Int64("id"))
   765  	if err != nil {
   766  		return err
   767  	}
   768  
   769  	return auth_service.DeleteSource(source)
   770  }