From eec9a4742a98093f7cb248998c99f3455a846401 Mon Sep 17 00:00:00 2001 From: Miguel de la Cruz Date: Thu, 8 Aug 2024 12:18:21 +0200 Subject: [PATCH] Allows creating new remote clusters without providing a password (#27864) * Allows creating new remote clusters without providing a password If the endpoint receives a request with no password, it will generate one internally and return it in the response, so the frotend can show it to the user. * Use a random string instead of a UUID for the generated password * Update function name to avoid CString reference and adds assertion * Update server/channels/utils/textgeneration.go Co-authored-by: Eva Sarafianou * Extends the charset --------- Co-authored-by: Eva Sarafianou --- api/v4/source/remoteclusters.yaml | 11 ++++-- server/channels/api4/remote_cluster.go | 20 +++++++---- server/channels/api4/remote_cluster_test.go | 37 ++++++++++++++++----- server/channels/utils/textgeneration.go | 20 +++++++++-- server/public/model/remote_cluster.go | 1 + 5 files changed, 69 insertions(+), 20 deletions(-) diff --git a/api/v4/source/remoteclusters.yaml b/api/v4/source/remoteclusters.yaml index 6e0d843fdc..25c1ec068b 100644 --- a/api/v4/source/remoteclusters.yaml +++ b/api/v4/source/remoteclusters.yaml @@ -81,7 +81,6 @@ type: object required: - name - - password properties: name: type: string @@ -89,7 +88,10 @@ type: string password: type: string - description: The password to use in the invite code. + description: | + The password to use in the invite code. If empty, + the server will generate one and it will be part + of the response responses: "201": description: Remote cluster creation successful @@ -103,6 +105,11 @@ invite: type: string description: The encrypted invite for the newly created remote cluster + password: + type: string + description: | + The password generated by the server if none was + sent on the create request "401": $ref: "#/components/responses/Unauthorized" "403": diff --git a/server/channels/api4/remote_cluster.go b/server/channels/api4/remote_cluster.go index 690dbd8f3d..2ecb10dd11 100644 --- a/server/channels/api4/remote_cluster.go +++ b/server/channels/api4/remote_cluster.go @@ -13,6 +13,7 @@ import ( "github.com/mattermost/mattermost/server/public/shared/mlog" "github.com/mattermost/mattermost/server/v8/channels/app" "github.com/mattermost/mattermost/server/v8/channels/audit" + "github.com/mattermost/mattermost/server/v8/channels/utils" "github.com/mattermost/mattermost/server/v8/platform/services/remotecluster" ) @@ -366,11 +367,6 @@ func createRemoteCluster(c *Context, w http.ResponseWriter, r *http.Request) { return } - if rcWithTeamAndPassword.Password == "" { - c.SetInvalidParam("password") - return - } - url := c.App.GetSiteURL() if url == "" { c.Err = model.NewAppError("createRemoteCluster", "api.get_site_url_error", nil, "", http.StatusUnprocessableEntity) @@ -398,7 +394,12 @@ func createRemoteCluster(c *Context, w http.ResponseWriter, r *http.Request) { } rcSaved.Sanitize() - inviteCode, iErr := c.App.CreateRemoteClusterInvite(rcSaved.RemoteId, url, rcSaved.Token, rcWithTeamAndPassword.Password) + password := rcWithTeamAndPassword.Password + if password == "" { + password = utils.SecureRandString(16) + } + + inviteCode, iErr := c.App.CreateRemoteClusterInvite(rcSaved.RemoteId, url, rcSaved.Token, password) if iErr != nil { c.Err = iErr return @@ -408,7 +409,12 @@ func createRemoteCluster(c *Context, w http.ResponseWriter, r *http.Request) { auditRec.AddEventResultState(rcSaved) auditRec.AddEventObjectType("remotecluster") - b, err := json.Marshal(model.RemoteClusterWithInvite{RemoteCluster: rcSaved, Invite: inviteCode}) + resp := model.RemoteClusterWithInvite{RemoteCluster: rcSaved, Invite: inviteCode} + if rcWithTeamAndPassword.Password == "" { + resp.Password = password + } + + b, err := json.Marshal(resp) if err != nil { c.Err = model.NewAppError("createRemoteCluster", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err) return diff --git a/server/channels/api4/remote_cluster_test.go b/server/channels/api4/remote_cluster_test.go index 0be46e5983..1bbe3bbbbf 100644 --- a/server/channels/api4/remote_cluster_test.go +++ b/server/channels/api4/remote_cluster_test.go @@ -195,17 +195,35 @@ func TestCreateRemoteCluster(t *testing.T) { th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.SiteURL = "http://localhost:8065" }) - t.Run("Should enforce the presence of the password", func(t *testing.T) { + t.Run("Should generate a password if none is given", func(t *testing.T) { // clean the password and check the response - rcWithTeamAndPassword.Password = "" + rcWithTeamNoPassword := &model.RemoteClusterWithPassword{ + RemoteCluster: &model.RemoteCluster{ + Name: "remotecluster-nopasswd", + SiteURL: "http://no-passwd.example.com", + Token: model.NewId(), + }, + Password: "", + } - rcWithInvite, resp, err := th.SystemAdminClient.CreateRemoteCluster(context.Background(), rcWithTeamAndPassword) - CheckBadRequestStatus(t, resp) - require.Error(t, err) - require.Empty(t, rcWithInvite) + rcWithInvite, resp, err := th.SystemAdminClient.CreateRemoteCluster(context.Background(), rcWithTeamNoPassword) + CheckCreatedStatus(t, resp) + require.NoError(t, err) + require.NotZero(t, rcWithInvite.Invite) + // when the password is not provided, it is returned as part + // of the response + require.NotZero(t, rcWithInvite.Password) + require.Len(t, rcWithInvite.Password, 16) - // reset password for the next tests - rcWithTeamAndPassword.Password = "mysupersecret" + rc, appErr := th.App.GetRemoteCluster(rcWithInvite.RemoteCluster.RemoteId) + require.Nil(t, appErr) + require.Equal(t, rcWithTeamNoPassword.Name, rc.Name) + + rci, appErr := th.App.DecryptRemoteClusterInvite(rcWithInvite.Invite, rcWithInvite.Password) + require.Nil(t, appErr) + require.Equal(t, rc.RemoteId, rci.RemoteId) + require.Equal(t, rc.RemoteToken, rci.Token) + require.Equal(t, th.App.GetSiteURL(), rci.SiteURL) }) t.Run("Should return a sanitized remote cluster and its invite", func(t *testing.T) { @@ -216,6 +234,9 @@ func TestCreateRemoteCluster(t *testing.T) { require.NotZero(t, rcWithInvite.Invite) require.Zero(t, rcWithInvite.RemoteCluster.Token) require.Zero(t, rcWithInvite.RemoteCluster.RemoteToken) + // when the password is provided as an input, is not returned + // by the endpoint + require.Zero(t, rcWithInvite.Password) rc, appErr := th.App.GetRemoteCluster(rcWithInvite.RemoteCluster.RemoteId) require.Nil(t, appErr) diff --git a/server/channels/utils/textgeneration.go b/server/channels/utils/textgeneration.go index db9e3af62e..4a8f5f8e87 100644 --- a/server/channels/utils/textgeneration.go +++ b/server/channels/utils/textgeneration.go @@ -4,7 +4,9 @@ package utils import ( - "math/rand" + crand "crypto/rand" + "math/big" + mrand "math/rand" "strings" ) @@ -468,11 +470,23 @@ Up to and hey without pill that this squid alas brusque on inventoried and sprea func RandString(l int, charset string) string { ret := make([]byte, l) for i := 0; i < l; i++ { - ret[i] = charset[rand.Intn(len(charset))] + ret[i] = charset[mrand.Intn(len(charset))] } return string(ret) } +func SecureRandString(n int) string { + charset := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%&*0123456789" + + var str strings.Builder + for i := 0; i < n; i++ { + num, _ := crand.Int(crand.Reader, big.NewInt(int64(len(charset)))) + str.WriteString(string(charset[num.Int64()])) + } + + return str.String() +} + // func RandomEmail(length Range, charset string) string { // emaillen := RandIntFromRange(length) // username := RandString(emaillen, charset) @@ -517,7 +531,7 @@ func RandomText(length Range, hashtags Range, mentions Range, users []string) st // Shuffle the words for i := range words { - j := rand.Intn(i + 1) + j := mrand.Intn(i + 1) words[i], words[j] = words[j], words[i] } diff --git a/server/public/model/remote_cluster.go b/server/public/model/remote_cluster.go index b2411fc8e8..590a22b757 100644 --- a/server/public/model/remote_cluster.go +++ b/server/public/model/remote_cluster.go @@ -155,6 +155,7 @@ type RemoteClusterWithPassword struct { type RemoteClusterWithInvite struct { RemoteCluster *RemoteCluster `json:"remote_cluster"` Invite string `json:"invite"` + Password string `json:"password,omitempty"` } func newIDFromBytes(b []byte) string {