From c30fea5f2d6e1a9936020693e2c862e487fa2ec5 Mon Sep 17 00:00:00 2001 From: Ibrahim Serdar Acikgoz Date: Mon, 31 Aug 2020 14:56:36 +0300 Subject: [PATCH] [MM-25645] api4/user: add migrate_auth endpoints (#14966) * api4/user: add migrate_auth endpoints * api4/user: reflect review comments * add translations Co-authored-by: Mattermod --- api4/user.go | 101 +++++++++++++++++++++++++++++++++++++++++++++ api4/user_local.go | 3 ++ api4/user_test.go | 26 ++++++++++++ i18n/en.json | 8 ++++ model/client4.go | 26 ++++++++++++ 5 files changed, 164 insertions(+) diff --git a/api4/user.go b/api4/user.go index 3b9af58d08..3d3647b575 100644 --- a/api4/user.go +++ b/api4/user.go @@ -85,6 +85,9 @@ func (api *API) InitUser() { api.BaseRoutes.Users.Handle("/tokens/enable", api.ApiSessionRequired(enableUserAccessToken)).Methods("POST") api.BaseRoutes.User.Handle("/typing", api.ApiSessionRequiredDisableWhenBusy(publishUserTyping)).Methods("POST") + + api.BaseRoutes.Users.Handle("/migrate_auth/ldap", api.ApiSessionRequired(migrateAuthToLDAP)).Methods("POST") + api.BaseRoutes.Users.Handle("/migrate_auth/saml", api.ApiSessionRequired(migrateAuthToSaml)).Methods("POST") } func createUser(c *Context, w http.ResponseWriter, r *http.Request) { @@ -2555,3 +2558,101 @@ func convertUserToBot(c *Context, w http.ResponseWriter, r *http.Request) { w.Write(bot.ToJson()) } + +func migrateAuthToLDAP(c *Context, w http.ResponseWriter, r *http.Request) { + props := model.StringInterfaceFromJson(r.Body) + from, ok := props["from"].(string) + if !ok { + c.SetInvalidParam("from") + return + } + if len(from) == 0 || (from != "email" && from != "gitlab" && from != "saml" && from != "google" && from != "office365") { + c.SetInvalidParam("from") + return + } + + force, ok := props["force"].(bool) + if !ok { + c.SetInvalidParam("force") + return + } + + matchField, ok := props["match_field"].(string) + if !ok { + c.SetInvalidParam("match_field") + return + } + + auditRec := c.MakeAuditRecord("migrateAuthToLdap", audit.Fail) + defer c.LogAuditRec(auditRec) + auditRec.AddMeta("from", from) + auditRec.AddMeta("match_field", matchField) + auditRec.AddMeta("force", force) + + if !c.App.SessionHasPermissionTo(*c.App.Session(), model.PERMISSION_MANAGE_SYSTEM) { + c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM) + return + } + + if migrate := c.App.AccountMigration(); migrate != nil { + if err := migrate.MigrateToLdap(from, matchField, force, false); err != nil { + c.Err = model.NewAppError("api.migrateAuthToLdap", "api.migrate_to_saml.error", nil, err.Error(), http.StatusInternalServerError) + return + } + } else { + c.Err = model.NewAppError("api.migrateAuthToLdap", "api.admin.ldap.not_available.app_error", nil, "", http.StatusNotImplemented) + return + } + + auditRec.Success() + ReturnStatusOK(w) +} + +func migrateAuthToSaml(c *Context, w http.ResponseWriter, r *http.Request) { + props := model.StringInterfaceFromJson(r.Body) + from, ok := props["from"].(string) + if !ok { + c.SetInvalidParam("from") + return + } + if len(from) == 0 || (from != "email" && from != "gitlab" && from != "ldap" && from != "google" && from != "office365") { + c.SetInvalidParam("from") + return + } + + auto, ok := props["auto"].(bool) + if !ok { + c.SetInvalidParam("auto") + return + } + matches, ok := props["matches"].(map[string]interface{}) + if !ok { + c.SetInvalidParam("matches") + return + } + usersMap := model.MapFromJson(strings.NewReader(model.StringInterfaceToJson(matches))) + + auditRec := c.MakeAuditRecord("migrateAuthToSaml", audit.Fail) + defer c.LogAuditRec(auditRec) + auditRec.AddMeta("from", from) + auditRec.AddMeta("matches", matches) + auditRec.AddMeta("auto", auto) + + if !c.App.SessionHasPermissionTo(*c.App.Session(), model.PERMISSION_MANAGE_SYSTEM) { + c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM) + return + } + + if migrate := c.App.AccountMigration(); migrate != nil { + if err := migrate.MigrateToSaml(from, usersMap, auto, false); err != nil { + c.Err = model.NewAppError("api.migrateAuthToSaml", "api.migrate_to_saml.error", nil, err.Error(), http.StatusInternalServerError) + return + } + } else { + c.Err = model.NewAppError("api.migrateAuthToSaml", "api.admin.saml.not_available.app_error", nil, "", http.StatusNotImplemented) + return + } + + auditRec.Success() + ReturnStatusOK(w) +} diff --git a/api4/user_local.go b/api4/user_local.go index 6b36dd5e3c..abdc5c0cb2 100644 --- a/api4/user_local.go +++ b/api4/user_local.go @@ -35,6 +35,9 @@ func (api *API) InitUserLocal() { api.BaseRoutes.Users.Handle("/tokens/revoke", api.ApiLocal(revokeUserAccessToken)).Methods("POST") api.BaseRoutes.User.Handle("/tokens", api.ApiLocal(getUserAccessTokensForUser)).Methods("GET") api.BaseRoutes.User.Handle("/tokens", api.ApiLocal(createUserAccessToken)).Methods("POST") + + api.BaseRoutes.Users.Handle("/migrate_auth/ldap", api.ApiLocal(migrateAuthToLDAP)).Methods("POST") + api.BaseRoutes.Users.Handle("/migrate_auth/saml", api.ApiLocal(migrateAuthToSaml)).Methods("POST") } func localGetUsers(c *Context, w http.ResponseWriter, r *http.Request) { diff --git a/api4/user_test.go b/api4/user_test.go index 4a62876fd7..5f87bdc551 100644 --- a/api4/user_test.go +++ b/api4/user_test.go @@ -5182,3 +5182,29 @@ func TestConvertUserToBot(t *testing.T) { require.NotNil(t, bot) }) } + +func TestMigrateAuthToLDAP(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + _, err := th.Client.MigrateAuthToLdap("email", "a", false) + CheckForbiddenStatus(t, err) + + th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) { + _, err = client.MigrateAuthToLdap("email", "a", false) + CheckNotImplementedStatus(t, err) + }) +} + +func TestMigrateAuthToSAML(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + _, err := th.Client.MigrateAuthToSaml("email", map[string]string{"1": "a"}, true) + CheckForbiddenStatus(t, err) + + th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) { + _, err = client.MigrateAuthToSaml("email", map[string]string{"1": "a"}, true) + CheckNotImplementedStatus(t, err) + }) +} diff --git a/i18n/en.json b/i18n/en.json index 3655e3d558..97cf69d52f 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -75,6 +75,10 @@ "id": "api.admin.get_brand_image.storage.app_error", "translation": "Image storage is not configured." }, + { + "id": "api.admin.ldap.not_available.app_error", + "translation": "LDAP is not available." + }, { "id": "api.admin.remove_certificate.delete.app_error", "translation": "An error occurred while deleting the certificate." @@ -1548,6 +1552,10 @@ "id": "api.marshal_error", "translation": "marshal error" }, + { + "id": "api.migrate_to_saml.error", + "translation": "Unable to migrate SAML." + }, { "id": "api.oauth.allow_oauth.redirect_callback.app_error", "translation": "invalid_request: Supplied redirect_uri did not match registered callback_url." diff --git a/model/client4.go b/model/client4.go index 91e408dd46..5034b6e79a 100644 --- a/model/client4.go +++ b/model/client4.go @@ -4053,6 +4053,32 @@ func (c *Client4) GetGroupsByUserId(userId string) ([]*Group, *Response) { return GroupsFromJson(r.Body), BuildResponse(r) } +func (c *Client4) MigrateAuthToLdap(fromAuthService string, matchField string, force bool) (bool, *Response) { + r, err := c.DoApiPost(c.GetUsersRoute()+"/migrate_auth/ldap", StringInterfaceToJson(map[string]interface{}{ + "from": fromAuthService, + "force": force, + "match_field": matchField, + })) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +func (c *Client4) MigrateAuthToSaml(fromAuthService string, usersMap map[string]string, auto bool) (bool, *Response) { + r, err := c.DoApiPost(c.GetUsersRoute()+"/migrate_auth/saml", StringInterfaceToJson(map[string]interface{}{ + "from": fromAuthService, + "auto": auto, + "matches": usersMap, + })) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + // Audits Section // GetAudits returns a list of audits for the whole system.