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 }