github.com/masterhung0112/hk_server/v5@v5.0.0-20220302090640-ec71aef15e1c/app/slashcommands/command_remote.go (about)

     1  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
     2  // See LICENSE.txt for license information.
     3  
     4  package slashcommands
     5  
     6  import (
     7  	"encoding/base64"
     8  	"errors"
     9  	"fmt"
    10  	"strings"
    11  
    12  	"github.com/masterhung0112/hk_server/v5/app"
    13  	"github.com/masterhung0112/hk_server/v5/app/request"
    14  	"github.com/masterhung0112/hk_server/v5/model"
    15  	"github.com/masterhung0112/hk_server/v5/shared/i18n"
    16  )
    17  
    18  const (
    19  	AvailableRemoteActions = "create, accept, remove, status"
    20  )
    21  
    22  type RemoteProvider struct {
    23  }
    24  
    25  const (
    26  	CommandTriggerRemote = "secure-connection"
    27  )
    28  
    29  func init() {
    30  	app.RegisterCommandProvider(&RemoteProvider{})
    31  }
    32  
    33  func (rp *RemoteProvider) GetTrigger() string {
    34  	return CommandTriggerRemote
    35  }
    36  
    37  func (rp *RemoteProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
    38  
    39  	remote := model.NewAutocompleteData(rp.GetTrigger(), "[action]", T("api.command_remote.remote_add_remove.help", map[string]interface{}{"Actions": AvailableRemoteActions}))
    40  
    41  	create := model.NewAutocompleteData("create", "", T("api.command_remote.invite.help"))
    42  	create.AddNamedTextArgument("name", T("api.command_remote.name.help"), T("api.command_remote.name.hint"), "", true)
    43  	create.AddNamedTextArgument("displayname", T("api.command_remote.displayname.help"), T("api.command_remote.displayname.hint"), "", false)
    44  	create.AddNamedTextArgument("password", T("api.command_remote.invite_password.help"), T("api.command_remote.invite_password.hint"), "", true)
    45  
    46  	accept := model.NewAutocompleteData("accept", "", T("api.command_remote.accept.help"))
    47  	accept.AddNamedTextArgument("name", T("api.command_remote.name.help"), T("api.command_remote.name.hint"), "", true)
    48  	accept.AddNamedTextArgument("displayname", T("api.command_remote.displayname.help"), T("api.command_remote.displayname.hint"), "", false)
    49  	accept.AddNamedTextArgument("password", T("api.command_remote.invite_password.help"), T("api.command_remote.invite_password.hint"), "", true)
    50  	accept.AddNamedTextArgument("invite", T("api.command_remote.invitation.help"), T("api.command_remote.invitation.hint"), "", true)
    51  
    52  	remove := model.NewAutocompleteData("remove", "", T("api.command_remote.remove.help"))
    53  	remove.AddNamedDynamicListArgument("connectionID", T("api.command_remote.remove_remote_id.help"), "builtin:"+CommandTriggerRemote, true)
    54  
    55  	status := model.NewAutocompleteData("status", "", T("api.command_remote.status.help"))
    56  
    57  	remote.AddCommand(create)
    58  	remote.AddCommand(accept)
    59  	remote.AddCommand(remove)
    60  	remote.AddCommand(status)
    61  
    62  	return &model.Command{
    63  		Trigger:          rp.GetTrigger(),
    64  		AutoComplete:     true,
    65  		AutoCompleteDesc: T("api.command_remote.desc"),
    66  		AutoCompleteHint: T("api.command_remote.hint"),
    67  		DisplayName:      T("api.command_remote.name"),
    68  		AutocompleteData: remote,
    69  	}
    70  }
    71  
    72  func (rp *RemoteProvider) DoCommand(a *app.App, c *request.Context, args *model.CommandArgs, message string) *model.CommandResponse {
    73  	if !a.HasPermissionTo(args.UserId, model.PERMISSION_MANAGE_SECURE_CONNECTIONS) {
    74  		return responsef(args.T("api.command_remote.permission_required", map[string]interface{}{"Permission": "manage_secure_connections"}))
    75  	}
    76  
    77  	margs := parseNamedArgs(args.Command)
    78  	action, ok := margs[ActionKey]
    79  	if !ok {
    80  		return responsef(args.T("api.command_remote.missing_command", map[string]interface{}{"Actions": AvailableRemoteActions}))
    81  	}
    82  
    83  	switch action {
    84  	case "create":
    85  		return rp.doCreate(a, args, margs)
    86  	case "accept":
    87  		return rp.doAccept(a, args, margs)
    88  	case "remove":
    89  		return rp.doRemove(a, args, margs)
    90  	case "status":
    91  		return rp.doStatus(a, args, margs)
    92  	}
    93  
    94  	return responsef(args.T("api.command_remote.unknown_action", map[string]interface{}{"Action": action}))
    95  }
    96  
    97  func (rp *RemoteProvider) GetAutoCompleteListItems(a *app.App, commandArgs *model.CommandArgs, arg *model.AutocompleteArg, parsed, toBeParsed string) ([]model.AutocompleteListItem, error) {
    98  	if !a.HasPermissionTo(commandArgs.UserId, model.PERMISSION_MANAGE_SECURE_CONNECTIONS) {
    99  		return nil, errors.New("You require `manage_secure_connections` permission to manage secure connections.")
   100  	}
   101  
   102  	if arg.Name == "connectionID" && strings.Contains(parsed, " remove ") {
   103  		return getRemoteClusterAutocompleteListItems(a, true)
   104  	}
   105  
   106  	return nil, fmt.Errorf("`%s` is not a dynamic argument", arg.Name)
   107  }
   108  
   109  // doCreate creates and displays an encrypted invite that can be used by a remote site to establish a simple trust.
   110  func (rp *RemoteProvider) doCreate(a *app.App, args *model.CommandArgs, margs map[string]string) *model.CommandResponse {
   111  	password := margs["password"]
   112  	if password == "" {
   113  		return responsef(args.T("api.command_remote.missing_empty", map[string]interface{}{"Arg": "password"}))
   114  	}
   115  
   116  	name := margs["name"]
   117  	if name == "" {
   118  		return responsef(args.T("api.command_remote.missing_empty", map[string]interface{}{"Arg": "name"}))
   119  	}
   120  
   121  	displayname := margs["displayname"]
   122  	if displayname == "" {
   123  		displayname = name
   124  	}
   125  
   126  	url := a.GetSiteURL()
   127  	if url == "" {
   128  		return responsef(args.T("api.command_remote.site_url_not_set"))
   129  	}
   130  
   131  	rc := &model.RemoteCluster{
   132  		Name:        name,
   133  		DisplayName: displayname,
   134  		Token:       model.NewId(),
   135  		CreatorId:   args.UserId,
   136  	}
   137  
   138  	rcSaved, appErr := a.AddRemoteCluster(rc)
   139  	if appErr != nil {
   140  		return responsef(args.T("api.command_remote.add_remote.error", map[string]interface{}{"Error": appErr.Error()}))
   141  	}
   142  
   143  	// Display the encrypted invitation
   144  	invite := &model.RemoteClusterInvite{
   145  		RemoteId:     rcSaved.RemoteId,
   146  		RemoteTeamId: args.TeamId,
   147  		SiteURL:      url,
   148  		Token:        rcSaved.Token,
   149  	}
   150  	encrypted, err := invite.Encrypt(password)
   151  	if err != nil {
   152  		return responsef(args.T("api.command_remote.encrypt_invitation.error", map[string]interface{}{"Error": err.Error()}))
   153  	}
   154  	encoded := base64.URLEncoding.EncodeToString(encrypted)
   155  
   156  	return responsef("##### " + args.T("api.command_remote.invitation_created") + "\n" +
   157  		args.T("api.command_remote.invite_summary", map[string]interface{}{"Command": "/secure-connection accept", "Invitation": encoded, "SiteURL": invite.SiteURL}))
   158  }
   159  
   160  // doAccept accepts an invitation generated by a remote site.
   161  func (rp *RemoteProvider) doAccept(a *app.App, args *model.CommandArgs, margs map[string]string) *model.CommandResponse {
   162  	password := margs["password"]
   163  	if password == "" {
   164  		return responsef(args.T("api.command_remote.missing_empty", map[string]interface{}{"Arg": "password"}))
   165  	}
   166  
   167  	name := margs["name"]
   168  	if name == "" {
   169  		return responsef(args.T("api.command_remote.missing_empty", map[string]interface{}{"Arg": "name"}))
   170  	}
   171  
   172  	displayname := margs["displayname"]
   173  	if displayname == "" {
   174  		displayname = name
   175  	}
   176  
   177  	blob := margs["invite"]
   178  	if blob == "" {
   179  		return responsef(args.T("api.command_remote.missing_empty", map[string]interface{}{"Arg": "invite"}))
   180  	}
   181  
   182  	// invite is encoded as base64 and encrypted
   183  	decoded, err := base64.URLEncoding.DecodeString(blob)
   184  	if err != nil {
   185  		return responsef(args.T("api.command_remote.decode_invitation.error", map[string]interface{}{"Error": err.Error()}))
   186  	}
   187  	invite := &model.RemoteClusterInvite{}
   188  	err = invite.Decrypt(decoded, password)
   189  	if err != nil {
   190  		return responsef(args.T("api.command_remote.incorrect_password.error", map[string]interface{}{"Error": err.Error()}))
   191  	}
   192  
   193  	rcs, _ := a.GetRemoteClusterService()
   194  	if rcs == nil {
   195  		return responsef(args.T("api.command_remote.service_not_enabled"))
   196  	}
   197  
   198  	url := a.GetSiteURL()
   199  	if url == "" {
   200  		return responsef(args.T("api.command_remote.site_url_not_set"))
   201  	}
   202  
   203  	rc, err := rcs.AcceptInvitation(invite, name, displayname, args.UserId, args.TeamId, url)
   204  	if err != nil {
   205  		return responsef(args.T("api.command_remote.accept_invitation.error", map[string]interface{}{"Error": err.Error()}))
   206  	}
   207  
   208  	return responsef("##### " + args.T("api.command_remote.accept_invitation", map[string]interface{}{"SiteURL": rc.SiteURL}))
   209  }
   210  
   211  // doRemove removes a remote cluster from the database, effectively revoking the trust relationship.
   212  func (rp *RemoteProvider) doRemove(a *app.App, args *model.CommandArgs, margs map[string]string) *model.CommandResponse {
   213  	id, ok := margs["connectionID"]
   214  	if !ok {
   215  		return responsef(args.T("api.command_remote.missing_empty", map[string]interface{}{"Arg": "remoteId"}))
   216  	}
   217  
   218  	deleted, err := a.DeleteRemoteCluster(id)
   219  	if err != nil {
   220  		responsef(args.T("api.command_remote.remove_remote.error", map[string]interface{}{"Error": err.Error()}))
   221  	}
   222  
   223  	result := "removed"
   224  	if !deleted {
   225  		result = "**NOT FOUND**"
   226  	}
   227  	return responsef("##### " + args.T("api.command_remote.cluster_removed", map[string]interface{}{"RemoteId": id, "Result": result}))
   228  }
   229  
   230  // doStatus displays connection status for all remote clusters.
   231  func (rp *RemoteProvider) doStatus(a *app.App, args *model.CommandArgs, _ map[string]string) *model.CommandResponse {
   232  	list, err := a.GetAllRemoteClusters(model.RemoteClusterQueryFilter{})
   233  	if err != nil {
   234  		responsef(args.T("api.command_remote.fetch_status.error", map[string]interface{}{"Error": err.Error()}))
   235  	}
   236  
   237  	if len(list) == 0 {
   238  		return responsef("** " + args.T("api.command_remote.remotes_not_found") + " **")
   239  	}
   240  
   241  	var sb strings.Builder
   242  	fmt.Fprintf(&sb, args.T("api.command_remote.remote_table_header")+" \n")
   243  	// | Secure Connection | Display name | ConnectionID | Site URL | Invite accepted | Online | Last ping  |
   244  	fmt.Fprintf(&sb, "| :---- | :---- | :---- | :---- | :---- | :---- | :---- | \n")
   245  
   246  	for _, rc := range list {
   247  		accepted := formatBool(args.T, rc.SiteURL != "")
   248  		online := formatBool(args.T, isOnline(rc.LastPingAt))
   249  		lastPing := formatTimestamp(rc.LastPingAt)
   250  
   251  		fmt.Fprintf(&sb, "| %s | %s | %s | %s | %s | %s | %s |\n", rc.Name, rc.DisplayName, rc.RemoteId, rc.SiteURL, accepted, online, lastPing)
   252  	}
   253  	return responsef(sb.String())
   254  }
   255  
   256  func isOnline(lastPing int64) bool {
   257  	return lastPing > model.GetMillis()-model.RemoteOfflineAfterMillis
   258  }
   259  
   260  func getRemoteClusterAutocompleteListItems(a *app.App, includeOffline bool) ([]model.AutocompleteListItem, error) {
   261  	filter := model.RemoteClusterQueryFilter{
   262  		ExcludeOffline: !includeOffline,
   263  	}
   264  	clusters, err := a.GetAllRemoteClusters(filter)
   265  	if err != nil || len(clusters) == 0 {
   266  		return []model.AutocompleteListItem{}, nil
   267  	}
   268  
   269  	list := make([]model.AutocompleteListItem, 0, len(clusters))
   270  
   271  	for _, rc := range clusters {
   272  		item := model.AutocompleteListItem{
   273  			Item:     rc.RemoteId,
   274  			HelpText: fmt.Sprintf("%s  (%s)", rc.DisplayName, rc.SiteURL)}
   275  		list = append(list, item)
   276  	}
   277  	return list, nil
   278  }
   279  
   280  func getRemoteClusterAutocompleteListItemsNotInChannel(a *app.App, channelId string, includeOffline bool) ([]model.AutocompleteListItem, error) {
   281  	filter := model.RemoteClusterQueryFilter{
   282  		ExcludeOffline: !includeOffline,
   283  		NotInChannel:   channelId,
   284  	}
   285  	all, err := a.GetAllRemoteClusters(filter)
   286  	if err != nil || len(all) == 0 {
   287  		return []model.AutocompleteListItem{}, nil
   288  	}
   289  
   290  	list := make([]model.AutocompleteListItem, 0, len(all))
   291  
   292  	for _, rc := range all {
   293  		item := model.AutocompleteListItem{
   294  			Item:     rc.RemoteId,
   295  			HelpText: fmt.Sprintf("%s  (%s)", rc.DisplayName, rc.SiteURL)}
   296  		list = append(list, item)
   297  	}
   298  	return list, nil
   299  }