diff --git a/api/admin_test.go b/api/admin_test.go index 933c3d59c3..f3d3ec4ed5 100644 --- a/api/admin_test.go +++ b/api/admin_test.go @@ -457,7 +457,8 @@ func TestAdminResetPassword(t *testing.T) { t.Fatal("Should have errored - password too short") } - user2 := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", AuthData: "1", AuthService: "random"} + authData := model.NewId() + user2 := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", AuthData: &authData, AuthService: "random"} user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) LinkUserToTeam(user2, team) store.Must(Srv.Store.User().VerifyEmail(user2.Id)) diff --git a/api/authentication.go b/api/authentication.go index 10ed578e1d..9243947add 100644 --- a/api/authentication.go +++ b/api/authentication.go @@ -39,17 +39,17 @@ func checkUserPassword(user *model.User, password string) *model.AppError { } } -func checkLdapUserPasswordAndAllCriteria(ldapId, password, mfaToken string) (*model.User, *model.AppError) { +func checkLdapUserPasswordAndAllCriteria(ldapId *string, password string, mfaToken string) (*model.User, *model.AppError) { ldapInterface := einterfaces.GetLdapInterface() - if ldapInterface == nil { + if ldapInterface == nil || ldapId == nil { err := model.NewLocAppError("doLdapAuthentication", "api.user.login_ldap.not_available.app_error", nil, "") err.StatusCode = http.StatusNotImplemented return nil, err } var user *model.User - if ldapUser, err := ldapInterface.DoLogin(ldapId, password); err != nil { + if ldapUser, err := ldapInterface.DoLogin(*ldapId, password); err != nil { err.StatusCode = http.StatusUnauthorized return nil, err } else { diff --git a/api/oauth.go b/api/oauth.go index 0375f4e6f8..37ca5ce0a4 100644 --- a/api/oauth.go +++ b/api/oauth.go @@ -600,8 +600,11 @@ func CompleteSwitchWithOAuth(c *Context, w http.ResponseWriter, r *http.Request, return } else { ssoUser := provider.GetUserFromJson(userData) - authData = ssoUser.AuthData ssoEmail = ssoUser.Email + + if ssoUser.AuthData != nil { + authData = *ssoUser.AuthData + } } if len(authData) == 0 { @@ -628,7 +631,7 @@ func CompleteSwitchWithOAuth(c *Context, w http.ResponseWriter, r *http.Request, return } - if result := <-Srv.Store.User().UpdateAuthData(user.Id, service, authData, ssoEmail); result.Err != nil { + if result := <-Srv.Store.User().UpdateAuthData(user.Id, service, &authData, ssoEmail); result.Err != nil { c.Err = result.Err return } diff --git a/api/user.go b/api/user.go index c53a643c71..9e93ae7794 100644 --- a/api/user.go +++ b/api/user.go @@ -494,8 +494,11 @@ func getUserForLogin(loginId string, onlyLdap bool) (*model.User, *model.AppErro *utils.Cfg.EmailSettings.EnableSignInWithUsername && !onlyLdap, *utils.Cfg.EmailSettings.EnableSignInWithEmail && !onlyLdap, ldapAvailable, - ); result.Err != nil { - + ); result.Err != nil && result.Err.Id == "store.sql_user.get_for_login.multiple_users" { + // don't fall back to LDAP in this case since we already know there's an LDAP user, but that it shouldn't work + result.Err.StatusCode = http.StatusBadRequest + return nil, result.Err + } else if result.Err != nil { if !ldapAvailable { // failed to find user and no LDAP server to fall back on result.Err.StatusCode = http.StatusBadRequest @@ -535,7 +538,7 @@ func LoginByOAuth(c *Context, w http.ResponseWriter, r *http.Request, service st } var user *model.User - if result := <-Srv.Store.User().GetByAuth(authData, service); result.Err != nil { + if result := <-Srv.Store.User().GetByAuth(&authData, service); result.Err != nil { if result.Err.Id == store.MISSING_AUTH_ACCOUNT_ERROR { return CreateOAuthUser(c, w, r, service, bytes.NewReader(buf.Bytes()), "") } @@ -1289,7 +1292,8 @@ func updateUser(c *Context, w http.ResponseWriter, r *http.Request) { } rusers[0].Password = "" - rusers[0].AuthData = "" + rusers[0].AuthData = new(string) + *rusers[0].AuthData = "" w.Write([]byte(rusers[0].ToJson())) } } @@ -1337,7 +1341,7 @@ func updatePassword(c *Context, w http.ResponseWriter, r *http.Request) { user := result.Data.(*model.User) - if user.AuthData != "" { + if user.AuthData != nil && *user.AuthData != "" { c.LogAudit("failed - tried to update user password who was logged in through oauth") c.Err = model.NewLocAppError("updatePassword", "api.user.update_password.oauth.app_error", nil, "auth_service="+user.AuthService) c.Err.StatusCode = http.StatusBadRequest @@ -1653,7 +1657,7 @@ func sendPasswordReset(c *Context, w http.ResponseWriter, r *http.Request) { user = result.Data.(*model.User) } - if len(user.AuthData) != 0 { + if user.AuthData != nil && len(*user.AuthData) != 0 { c.Err = model.NewLocAppError("sendPasswordReset", "api.user.send_password_reset.sso.app_error", nil, "userId="+user.Id) return } @@ -1749,7 +1753,7 @@ func ResetPassword(c *Context, userId, newPassword string) *model.AppError { user = result.Data.(*model.User) } - if len(user.AuthData) != 0 && !c.IsSystemAdmin() { + if user.AuthData != nil && len(*user.AuthData) != 0 && !c.IsSystemAdmin() { return model.NewLocAppError("ResetPassword", "api.user.reset_password.sso.app_error", nil, "userId="+user.Id) } @@ -2148,13 +2152,13 @@ func ldapToEmail(c *Context, w http.ResponseWriter, r *http.Request) { } ldapInterface := einterfaces.GetLdapInterface() - if ldapInterface == nil { + if ldapInterface == nil || user.AuthData == nil { c.Err = model.NewLocAppError("ldapToEmail", "api.user.ldap_to_email.not_available.app_error", nil, "") c.Err.StatusCode = http.StatusNotImplemented return } - if err := ldapInterface.CheckPassword(user.AuthData, ldapPassword); err != nil { + if err := ldapInterface.CheckPassword(*user.AuthData, ldapPassword); err != nil { c.LogAuditWithUserId(user.Id, "fail - ldap authentication failed") c.Err = err return diff --git a/api/user_test.go b/api/user_test.go index 9dd57dc209..c34d32c11f 100644 --- a/api/user_test.go +++ b/api/user_test.go @@ -1109,7 +1109,8 @@ func TestSendPasswordReset(t *testing.T) { t.Fatal("Should have errored - bad email") } - user2 := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", AuthData: "1", AuthService: "random"} + authData := model.NewId() + user2 := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", AuthData: &authData, AuthService: "random"} user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) LinkUserToTeam(user2, team) store.Must(Srv.Store.User().VerifyEmail(user2.Id)) @@ -1178,7 +1179,8 @@ func TestResetPassword(t *testing.T) { recovery = result.Data.(*model.PasswordRecovery) } - if result := <-Srv.Store.User().UpdateAuthData(user.Id, "random", "1", ""); result.Err != nil { + authData := model.NewId() + if result := <-Srv.Store.User().UpdateAuthData(user.Id, "random", &authData, ""); result.Err != nil { t.Fatal(result.Err) } diff --git a/i18n/en.json b/i18n/en.json index 8a1634a4f9..267a32427b 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1341,7 +1341,7 @@ }, { "id": "api.templates.upgrade_30_body.info", - "translation": "
|
- |
- ||||
|
- |
-
- |
-
- |
-
- |
- - - | -
Channels organize conversations across different topics. They’re open to everyone on your team. To send private communications use Direct Messages for a single person or Private Groups for multiple people.
", diff --git a/webapp/i18n/es.json b/webapp/i18n/es.json index ce29d234bd..7aa3071f6b 100644 --- a/webapp/i18n/es.json +++ b/webapp/i18n/es.json @@ -1130,7 +1130,7 @@ "sidebar.direct": "Mensajes Directos", "sidebar.more": "Más", "sidebar.moreElips": "Más...", - "sidebar.otherMembers": "Otros equipos", + "sidebar.otherMembers": "Fuera de este equipo", "sidebar.pg": "Grupos Privados", "sidebar.removeList": "Remover de la lista", "sidebar.tutorialScreen1": "Canales organizan las conversaciones en diferentes tópicos. Son abiertos para cualquier persona de tu equipo. Para enviar comunicaciones privadas con una sola persona utiliza Mensajes Directos o con multiples personas utilizando Grupos Privados.
", @@ -1341,7 +1341,9 @@ "user.settings.general.title": "Configuración General", "user.settings.general.uploadImage": "Pinchar 'Editar' para subir una imagen.", "user.settings.general.username": "Nombre de usuario", + "user.settings.general.usernameInfo": "Escoge algo que sea fácil de reconocer y recordar para tus compañeros.", "user.settings.general.usernameReserved": "Este nombre de usuario está reservado, por favor escoge otro", + "user.settings.general.usernameRestrictions": "El nombre de usuario debe comenzar con una letra y debe contener entre {min} y {max} caracteres en minúscula creado con numeros, letras y los símbolos '.', '-', y '_'.", "user.settings.general.validEmail": "Por favor ingresa una dirección de correo electrónico válida", "user.settings.general.validImage": "Sólo pueden ser utilizadas imágenes JPG o PNG en el perfil", "user.settings.import_theme.cancel": "Cancelar", diff --git a/webapp/sass/responsive/_mobile.scss b/webapp/sass/responsive/_mobile.scss index 3a4cd3b893..cc3d7a4b9f 100644 --- a/webapp/sass/responsive/_mobile.scss +++ b/webapp/sass/responsive/_mobile.scss @@ -11,6 +11,15 @@ } } + .compliance-panel, + .audit-panel { + .row { + > .form-group { + padding-left: 15px; + } + } + } + .user-popover { pointer-events: none; } diff --git a/webapp/sass/routes/_admin-console.scss b/webapp/sass/routes/_admin-console.scss index 65fefdb331..0f47e7529e 100644 --- a/webapp/sass/routes/_admin-console.scss +++ b/webapp/sass/routes/_admin-console.scss @@ -160,17 +160,6 @@ width: 100%; } - .compliance__panel, - .audit__panel { - background-color: $white; - border: 1px solid $border-gray; - height: 70vh; - margin-top: 10px; - overflow: auto; - padding: 5px; - width: 100%; - } - .app__content { color: #333; diff --git a/webapp/sass/routes/_compliance.scss b/webapp/sass/routes/_compliance.scss new file mode 100644 index 0000000000..57eb538c67 --- /dev/null +++ b/webapp/sass/routes/_compliance.scss @@ -0,0 +1,38 @@ +@charset 'UTF-8'; + +.compliance-panel__table, +.audit-panel__table { + background-color: $white; + border: 1px solid $border-gray; + margin-top: 10px; + max-height: 70vh; + min-height: 100px; + overflow: auto; + padding: 5px; + width: 100%; +} + +.compliance-panel, +.audit-panel { + .row { + > .form-group { + padding-left: 0; + + &:first-child { + padding-left: 15px; + } + } + + label { + font-weight: 600; + } + } + + .fa-refresh { + margin-right: 5px; + } +} + +.compliance-panel { + margin-bottom: 3em; +} diff --git a/webapp/sass/routes/_module.scss b/webapp/sass/routes/_module.scss index 4f3f6f9cd4..11b815007f 100644 --- a/webapp/sass/routes/_module.scss +++ b/webapp/sass/routes/_module.scss @@ -4,6 +4,7 @@ @import 'activity-log'; @import 'admin-console'; @import 'backstage'; +@import 'compliance'; @import 'docs'; @import 'error-page'; @import 'loading'; diff --git a/webapp/stores/integration_store.jsx b/webapp/stores/integration_store.jsx index 12cbc3407e..454e6290bd 100644 --- a/webapp/stores/integration_store.jsx +++ b/webapp/stores/integration_store.jsx @@ -15,14 +15,11 @@ class IntegrationStore extends EventEmitter { this.dispatchToken = AppDispatcher.register(this.handleEventPayload.bind(this)); - this.incomingWebhooks = []; - this.receivedIncomingWebhooks = false; + this.incomingWebhooks = new Map(); - this.outgoingWebhooks = []; - this.receivedOutgoingWebhooks = false; + this.outgoingWebhooks = new Map(); - this.commands = []; - this.receivedCommands = false; + this.commands = new Map(); } addChangeListener(callback) { @@ -37,100 +34,119 @@ class IntegrationStore extends EventEmitter { this.emit(CHANGE_EVENT); } - hasReceivedIncomingWebhooks() { - return this.receivedIncomingWebhooks; + hasReceivedIncomingWebhooks(teamId) { + return this.incomingWebhooks.has(teamId); } - getIncomingWebhooks() { - return this.incomingWebhooks; + getIncomingWebhooks(teamId) { + return this.incomingWebhooks.get(teamId) || []; } - setIncomingWebhooks(incomingWebhooks) { - this.incomingWebhooks = incomingWebhooks; - this.receivedIncomingWebhooks = true; + setIncomingWebhooks(teamId, incomingWebhooks) { + this.incomingWebhooks.set(teamId, incomingWebhooks); } addIncomingWebhook(incomingWebhook) { - this.incomingWebhooks.push(incomingWebhook); + const teamId = incomingWebhook.team_id; + const incomingWebhooks = this.getIncomingWebhooks(teamId); + + incomingWebhooks.push(incomingWebhook); + + this.setIncomingWebhooks(teamId, incomingWebhooks); } - removeIncomingWebhook(id) { - for (let i = 0; i < this.incomingWebhooks.length; i++) { - if (this.incomingWebhooks[i].id === id) { - this.incomingWebhooks.splice(i, 1); - break; - } - } + removeIncomingWebhook(teamId, id) { + let incomingWebhooks = this.getIncomingWebhooks(teamId); + + incomingWebhooks = incomingWebhooks.filter((incomingWebhook) => incomingWebhook.id !== id); + + this.setIncomingWebhooks(teamId, incomingWebhooks); } - hasReceivedOutgoingWebhooks() { - return this.receivedOutgoingWebhooks; + hasReceivedOutgoingWebhooks(teamId) { + return this.outgoingWebhooks.has(teamId); } - getOutgoingWebhooks() { - return this.outgoingWebhooks; + getOutgoingWebhooks(teamId) { + return this.outgoingWebhooks.get(teamId) || []; } - setOutgoingWebhooks(outgoingWebhooks) { - this.outgoingWebhooks = outgoingWebhooks; - this.receivedOutgoingWebhooks = true; + setOutgoingWebhooks(teamId, outgoingWebhooks) { + this.outgoingWebhooks.set(teamId, outgoingWebhooks); } addOutgoingWebhook(outgoingWebhook) { - this.outgoingWebhooks.push(outgoingWebhook); + const teamId = outgoingWebhook.team_id; + const outgoingWebhooks = this.getOutgoingWebhooks(teamId); + + outgoingWebhooks.push(outgoingWebhook); + + this.setOutgoingWebhooks(teamId, outgoingWebhooks); } updateOutgoingWebhook(outgoingWebhook) { - for (let i = 0; i < this.outgoingWebhooks.length; i++) { - if (this.outgoingWebhooks[i].id === outgoingWebhook.id) { - this.outgoingWebhooks[i] = outgoingWebhook; + const teamId = outgoingWebhook.team_id; + const outgoingWebhooks = this.getOutgoingWebhooks(teamId); + + for (let i = 0; i < outgoingWebhooks.length; i++) { + if (outgoingWebhooks[i].id === outgoingWebhook.id) { + outgoingWebhooks[i] = outgoingWebhook; break; } } + + this.setOutgoingWebhooks(teamId, outgoingWebhooks); } - removeOutgoingWebhook(id) { - for (let i = 0; i < this.outgoingWebhooks.length; i++) { - if (this.outgoingWebhooks[i].id === id) { - this.outgoingWebhooks.splice(i, 1); - break; - } - } + removeOutgoingWebhook(teamId, id) { + let outgoingWebhooks = this.getOutgoingWebhooks(teamId); + + outgoingWebhooks = outgoingWebhooks.filter((outgoingWebhook) => outgoingWebhook.id !== id); + + this.setOutgoingWebhooks(teamId, outgoingWebhooks); } - hasReceivedCommands() { - return this.receivedCommands; + hasReceivedCommands(teamId) { + return this.commands.has(teamId); } - getCommands() { - return this.commands; + getCommands(teamId) { + return this.commands.get(teamId) || []; } - setCommands(commands) { - this.commands = commands; - this.receivedCommands = true; + setCommands(teamId, commands) { + this.commands.set(teamId, commands); } addCommand(command) { - this.commands.push(command); + const teamId = command.team_id; + const commands = this.getCommands(teamId); + + commands.push(command); + + this.setCommands(teamId, commands); } updateCommand(command) { - for (let i = 0; i < this.commands.length; i++) { - if (this.commands[i].id === command.id) { - this.commands[i] = command; + const teamId = command.team_id; + const commands = this.getCommands(teamId); + + for (let i = 0; i < commands.length; i++) { + if (commands[i].id === command.id) { + commands[i] = command; break; } } + + this.setCommands(teamId, commands); } - removeCommand(id) { - for (let i = 0; i < this.commands.length; i++) { - if (this.commands[i].id === id) { - this.commands.splice(i, 1); - break; - } - } + removeCommand(teamId, id) { + let commands = this.getCommands(teamId); + + commands = commands.filter((command) => command.id !== id); + + this.setCommands(teamId, commands); } handleEventPayload(payload) { @@ -138,7 +154,7 @@ class IntegrationStore extends EventEmitter { switch (action.type) { case ActionTypes.RECEIVED_INCOMING_WEBHOOKS: - this.setIncomingWebhooks(action.incomingWebhooks); + this.setIncomingWebhooks(action.teamId, action.incomingWebhooks); this.emitChange(); break; case ActionTypes.RECEIVED_INCOMING_WEBHOOK: @@ -146,11 +162,11 @@ class IntegrationStore extends EventEmitter { this.emitChange(); break; case ActionTypes.REMOVED_INCOMING_WEBHOOK: - this.removeIncomingWebhook(action.id); + this.removeIncomingWebhook(action.teamId, action.id); this.emitChange(); break; case ActionTypes.RECEIVED_OUTGOING_WEBHOOKS: - this.setOutgoingWebhooks(action.outgoingWebhooks); + this.setOutgoingWebhooks(action.teamId, action.outgoingWebhooks); this.emitChange(); break; case ActionTypes.RECEIVED_OUTGOING_WEBHOOK: @@ -162,11 +178,11 @@ class IntegrationStore extends EventEmitter { this.emitChange(); break; case ActionTypes.REMOVED_OUTGOING_WEBHOOK: - this.removeOutgoingWebhook(action.id); + this.removeOutgoingWebhook(action.teamId, action.id); this.emitChange(); break; case ActionTypes.RECEIVED_COMMANDS: - this.setCommands(action.commands); + this.setCommands(action.teamId, action.commands); this.emitChange(); break; case ActionTypes.RECEIVED_COMMAND: @@ -178,7 +194,7 @@ class IntegrationStore extends EventEmitter { this.emitChange(); break; case ActionTypes.REMOVED_COMMAND: - this.removeCommand(action.id); + this.removeCommand(action.teamId, action.id); this.emitChange(); break; } diff --git a/webapp/utils/async_client.jsx b/webapp/utils/async_client.jsx index a562964b14..6535c024d1 100644 --- a/webapp/utils/async_client.jsx +++ b/webapp/utils/async_client.jsx @@ -1145,6 +1145,7 @@ export function listIncomingHooks() { AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_INCOMING_WEBHOOKS, + teamId: Client.teamId, incomingWebhooks: data }); }, @@ -1168,6 +1169,7 @@ export function listOutgoingHooks() { AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_OUTGOING_WEBHOOKS, + teamId: Client.teamId, outgoingWebhooks: data }); }, @@ -1230,6 +1232,7 @@ export function deleteIncomingHook(id) { () => { AppDispatcher.handleServerAction({ type: ActionTypes.REMOVED_INCOMING_WEBHOOK, + teamId: Client.teamId, id }); }, @@ -1245,6 +1248,7 @@ export function deleteOutgoingHook(id) { () => { AppDispatcher.handleServerAction({ type: ActionTypes.REMOVED_OUTGOING_WEBHOOK, + teamId: Client.teamId, id }); }, @@ -1282,6 +1286,7 @@ export function listTeamCommands() { AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_COMMANDS, + teamId: Client.teamId, commands: data }); }, @@ -1321,6 +1326,7 @@ export function deleteCommand(id) { () => { AppDispatcher.handleServerAction({ type: ActionTypes.REMOVED_COMMAND, + teamId: Client.teamId, id }); },