mattermost-community-enterp.../cmd/mmctl/commands/auth.go
Claude ec1f89217a Merge: Complete Mattermost Server with Community Enterprise
Full Mattermost server source with integrated Community Enterprise features.
Includes vendor directory for offline/air-gapped builds.

Structure:
- enterprise-impl/: Enterprise feature implementations
- enterprise-community/: Init files that register implementations
- enterprise/: Bridge imports (community_imports.go)
- vendor/: All dependencies for offline builds

Build (online):
  go build ./cmd/mattermost

Build (offline/air-gapped):
  go build -mod=vendor ./cmd/mattermost

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 23:59:07 +09:00

448 lines
13 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package commands
import (
"bufio"
"context"
"fmt"
"net/http"
"net/url"
"os"
"slices"
"strings"
"syscall"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"golang.org/x/term"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
)
var AuthCmd = &cobra.Command{
Use: "auth",
Short: "Manages the credentials of the remote Mattermost instances",
}
var LoginCmd = &cobra.Command{
Use: "login [instance url] --name [server name] --username [username] --password-file [password-file]",
Short: "Login into an instance",
Long: "Login into an instance and store credentials",
Example: ` auth login https://mattermost.example.com
auth login https://mattermost.example.com --name local-server --username sysadmin --password-file mysupersecret.txt
auth login https://mattermost.example.com --name local-server --username sysadmin --password-file mysupersecret.txt --mfa-token 123456
auth login https://mattermost.example.com --name local-server --access-token myaccesstoken`,
Args: cobra.ExactArgs(1),
RunE: loginCmdF,
}
var CurrentCmd = &cobra.Command{
Use: "current",
Short: "Show current user credentials",
Long: "Show the currently stored user credentials",
Example: ` auth current`,
RunE: currentCmdF,
}
var SetCmd = &cobra.Command{
Use: "set [server name]",
Short: "Set the credentials to use",
Long: "Set an credentials to use in the following commands",
Example: ` auth set local-server`,
Args: cobra.ExactArgs(1),
RunE: setCmdF,
}
var ListCmd = &cobra.Command{
Use: "list",
Short: "Lists the credentials",
Long: "Print a list of the registered credentials",
Example: ` auth list`,
RunE: listCmdF,
}
var RenewCmd = &cobra.Command{
Use: "renew",
Short: "Renews a set of credentials",
Long: "Renews the credentials for a given server",
Example: ` auth renew local-server`,
Args: cobra.ExactArgs(1),
RunE: renewCmdF,
}
var DeleteCmd = &cobra.Command{
Use: "delete [server name]",
Short: "Delete an credentials",
Long: "Delete an credentials by its name",
Example: ` auth delete local-server`,
Args: cobra.ExactArgs(1),
RunE: deleteCmdF,
}
var CleanCmd = &cobra.Command{
Use: "clean",
Short: "Clean all credentials",
Long: "Clean the currently stored credentials",
Example: ` auth clean`,
RunE: cleanCmdF,
}
func init() {
LoginCmd.Flags().StringP("name", "n", "", "Name for the credentials")
LoginCmd.Flags().StringP("username", "u", "", "Username for the credentials")
LoginCmd.Flags().StringP("access-token", "a", "", "Access token to use instead of username/password")
_ = LoginCmd.Flags().MarkHidden("access-token")
LoginCmd.Flags().StringP("access-token-file", "t", "", "Access token file to be read to use instead of username/password")
LoginCmd.Flags().StringP("mfa-token", "m", "", "MFA token for the credentials")
LoginCmd.Flags().StringP("password", "p", "", "Password for the credentials")
_ = LoginCmd.Flags().MarkHidden("password")
LoginCmd.Flags().StringP("password-file", "f", "", "Password file to be read for the credentials")
LoginCmd.Flags().Bool("no-activate", false, "If present, it won't activate the credentials after login")
RenewCmd.Flags().StringP("password", "p", "", "Password for the credentials")
_ = RenewCmd.Flags().MarkHidden("password")
RenewCmd.Flags().StringP("password-file", "f", "", "Password file to be read for the credentials")
RenewCmd.Flags().StringP("access-token", "a", "", "Access token to use instead of username/password")
_ = RenewCmd.Flags().MarkHidden("access-token")
RenewCmd.Flags().StringP("access-token-file", "t", "", "Access token file to be read to use instead of username/password")
RenewCmd.Flags().StringP("mfa-token", "m", "", "MFA token for the credentials")
AuthCmd.AddCommand(
LoginCmd,
CurrentCmd,
SetCmd,
ListCmd,
RenewCmd,
DeleteCmd,
CleanCmd,
)
RootCmd.AddCommand(AuthCmd)
}
func loginCmdF(cmd *cobra.Command, args []string) error {
name, err := cmd.Flags().GetString("name")
if err != nil {
return err
}
username, err := cmd.Flags().GetString("username")
if err != nil {
return err
}
password, err := cmd.Flags().GetString("password")
if err != nil {
return err
}
passwordFile, _ := cmd.Flags().GetString("password-file")
if password != "" && passwordFile != "" {
return errors.New("cannot use two passwords at the same time")
}
if fErr := readSecretFromFile(passwordFile, &password); fErr != nil {
return fmt.Errorf("could not read the password: %w", fErr)
}
accessToken, err := cmd.Flags().GetString("access-token")
if err != nil {
return err
}
accessTokenFile, _ := cmd.Flags().GetString("access-token-file")
if accessToken != "" && accessTokenFile != "" {
return errors.New("cannot use two access tokens at the same time")
}
if fErr := readSecretFromFile(accessTokenFile, &accessToken); fErr != nil {
return fmt.Errorf("could not read the access-token: %w", fErr)
}
mfaToken, err := cmd.Flags().GetString("mfa-token")
if err != nil {
return err
}
allowInsecureSHA1 := viper.GetBool("insecure-sha1-intermediate")
allowInsecureTLS := viper.GetBool("insecure-tls-version")
instanceURL := strings.TrimRight(args[0], "/")
_, err = url.ParseRequestURI(instanceURL)
if err != nil {
return fmt.Errorf("could not parse the instance url: %w", err)
}
res, err := http.Get(instanceURL)
if err != nil {
return fmt.Errorf("could not get instance status: %w", err)
}
if res.StatusCode != 200 {
return fmt.Errorf("instance status code is not 200: %d", res.StatusCode)
}
method := MethodPassword
ctx := context.TODO()
if name == "" {
reader := bufio.NewReader(os.Stdin)
fmt.Printf("Connection name: ")
name, err = reader.ReadString('\n')
if err != nil {
return err
}
name = strings.TrimSpace(name)
}
if accessToken != "" && username != "" {
return errors.New("you must use --access-token or --username, but not both")
}
if accessToken == "" && username == "" {
reader := bufio.NewReader(os.Stdin)
fmt.Printf("Username: ")
username, err = reader.ReadString('\n')
if err != nil {
return err
}
username = strings.TrimSpace(username)
}
if username != "" && password == "" {
fmt.Printf("Password: ")
stdinPassword, err := getPasswordFromStdin()
if err != nil {
return errors.WithMessage(err, "couldn't read password")
}
password = stdinPassword
}
if username != "" {
var c *model.Client4
var err error
if mfaToken != "" {
c, _, err = InitClientWithMFA(ctx, username, password, mfaToken, instanceURL, allowInsecureSHA1, allowInsecureTLS)
method = MethodMFA
} else {
c, _, err = InitClientWithUsernameAndPassword(ctx, username, password, instanceURL, allowInsecureSHA1, allowInsecureTLS)
}
if err != nil {
return fmt.Errorf("could not initiate client: %w", err)
}
accessToken = c.AuthToken
} else {
username = "Personal Access Token"
method = MethodToken
credentials := Credentials{
InstanceURL: instanceURL,
AuthToken: accessToken,
}
if _, _, err := InitClientWithCredentials(ctx, &credentials, allowInsecureSHA1, allowInsecureTLS); err != nil {
return fmt.Errorf("could not initiate client: %w", err)
}
}
credentials := Credentials{
Name: name,
InstanceURL: instanceURL,
Username: username,
AuthToken: accessToken,
AuthMethod: method,
}
if err := SaveCredentials(credentials); err != nil {
return err
}
noActivate, _ := cmd.Flags().GetBool("no-activate")
if !noActivate {
if err := SetCurrent(name); err != nil {
return err
}
}
printer.Print(fmt.Sprintf("\n credentials for %q: \"%s@%s\" stored\n", name, username, instanceURL))
return nil
}
func getPasswordFromStdin() (string, error) {
// syscall.Stdin is of type int in all architectures but in
// windows, so we have to cast it to ensure cross compatibility
//nolint:unconvert
bytePassword, err := term.ReadPassword(int(syscall.Stdin))
fmt.Println("")
if err != nil {
return "", err
}
return string(bytePassword), nil
}
func currentCmdF(cmd *cobra.Command, args []string) error {
credentials, err := GetCurrentCredentials()
if err != nil {
return err
}
printer.Print(fmt.Sprintf("\n found credentials for %q: \"%s@%s\"\n", credentials.Name, credentials.Username, credentials.InstanceURL))
return nil
}
func setCmdF(cmd *cobra.Command, args []string) error {
if err := SetCurrent(args[0]); err != nil {
return err
}
printer.Print(fmt.Sprintf("Credentials for server %q set as active", args[0]))
return nil
}
func listCmdF(cmd *cobra.Command, args []string) error {
credentialsList, err := ReadCredentialsList()
if err != nil {
return err
}
if len(*credentialsList) == 0 {
return errors.New("there are no registered credentials, maybe you need to use login first")
}
serverNames := []string{}
nameTitle, usernameTitle, instanceURLTitle := "Name", "Username", "InstanceURL"
maxNameLen, maxUsernameLen, maxInstanceURLLen := len(nameTitle), len(usernameTitle), len(instanceURLTitle)
for _, c := range *credentialsList {
serverNames = append(serverNames, c.Name)
if maxNameLen <= len(c.Name) {
maxNameLen = len(c.Name)
}
if maxUsernameLen <= len(c.Username) {
maxUsernameLen = len(c.Username)
}
if maxInstanceURLLen <= len(c.InstanceURL) {
maxInstanceURLLen = len(c.InstanceURL)
}
}
slices.Sort(serverNames)
printer.Print(fmt.Sprintf("\n | Active | %*s | %*s | %*s |", maxNameLen, nameTitle, maxUsernameLen, usernameTitle, maxInstanceURLLen, instanceURLTitle))
printer.Print(fmt.Sprintf(" |%s|%s|%s|%s|", strings.Repeat("-", 8), strings.Repeat("-", maxNameLen+2), strings.Repeat("-", maxUsernameLen+2), strings.Repeat("-", maxInstanceURLLen+2)))
for _, name := range serverNames {
c := (*credentialsList)[name]
if c.Active {
printer.Print(fmt.Sprintf(" | * | %*s | %*s | %*s |", maxNameLen, c.Name, maxUsernameLen, c.Username, maxInstanceURLLen, c.InstanceURL))
} else {
printer.Print(fmt.Sprintf(" | | %*s | %*s | %*s |", maxNameLen, c.Name, maxUsernameLen, c.Username, maxInstanceURLLen, c.InstanceURL))
}
}
printer.Print("")
return nil
}
func renewCmdF(cmd *cobra.Command, args []string) error {
printer.SetSingle(true)
password, _ := cmd.Flags().GetString("password")
passwordFile, _ := cmd.Flags().GetString("password-file")
if password != "" && passwordFile != "" {
return errors.New("cannot use two passwords at the same time")
}
if fErr := readSecretFromFile(passwordFile, &password); fErr != nil {
return fmt.Errorf("could not read the password: %w", fErr)
}
accessToken, _ := cmd.Flags().GetString("access-token")
accessTokenFile, _ := cmd.Flags().GetString("access-token-file")
if accessToken != "" && accessTokenFile != "" {
return errors.New("cannot use two access tokens at the same time")
}
if fErr := readSecretFromFile(accessTokenFile, &accessToken); fErr != nil {
return fmt.Errorf("could not read the access-token: %w", fErr)
}
mfaToken, _ := cmd.Flags().GetString("mfa-token")
allowInsecureSHA1 := viper.GetBool("insecure-sha1-intermediate")
allowInsecureTLS := viper.GetBool("insecure-tls-version")
credentials, err := GetCredentials(args[0])
if err != nil {
return err
}
ctx := context.TODO()
if (credentials.AuthMethod == MethodPassword || credentials.AuthMethod == MethodMFA) && password == "" {
if password == "" {
fmt.Printf("Password: ")
stdinPassword, err := getPasswordFromStdin()
if err != nil {
return errors.WithMessage(err, "couldn't read password")
}
password = stdinPassword
}
}
switch credentials.AuthMethod {
case MethodPassword:
c, _, err := InitClientWithUsernameAndPassword(ctx, credentials.Username, password, credentials.InstanceURL, allowInsecureSHA1, allowInsecureTLS)
if err != nil {
return err
}
credentials.AuthToken = c.AuthToken
case MethodToken:
if accessToken == "" {
return errors.New("requires the --access-token parameter to be set")
}
credentials.AuthToken = accessToken
if _, _, err := InitClientWithCredentials(ctx, credentials, allowInsecureSHA1, allowInsecureTLS); err != nil {
return err
}
case MethodMFA:
if mfaToken == "" {
return errors.New("requires the --mfa-token parameter to be set")
}
c, _, err := InitClientWithMFA(ctx, credentials.Username, password, mfaToken, credentials.InstanceURL, allowInsecureSHA1, allowInsecureTLS)
if err != nil {
return err
}
credentials.AuthToken = c.AuthToken
default:
return errors.Errorf("invalid auth method %q", credentials.AuthMethod)
}
if err := SaveCredentials(*credentials); err != nil {
return err
}
printer.PrintT("Credentials for server \"{{.Name}}\" successfully renewed", credentials)
return nil
}
func deleteCmdF(cmd *cobra.Command, args []string) error {
credentialsList, err := ReadCredentialsList()
if err != nil {
return err
}
name := args[0]
credentials := (*credentialsList)[name]
if credentials == nil {
return errors.Errorf("cannot find credentials for server name %q", name)
}
delete(*credentialsList, name)
return SaveCredentialsList(credentialsList)
}
func cleanCmdF(cmd *cobra.Command, args []string) error {
if err := CleanCredentials(); err != nil {
return err
}
return nil
}