Files
mattermost/web/context.go
Martin Kraft 6d30b21dd2 Ldap groups phase1 (#9752)
* Initial models, API, app, and persistence of groups and group syncing.

* Consistent letter casing in ldif.

* Moves group-specific migrations into func.

* Adds API endpoint to retrieve LDAP groups (and associated MM groups) one tree level at a time.

* Adds mattermost group id to SCIMGroup (if available).

* Splits user and group creation so that memberOf works. Returns users from ldap interface.

* Updates method name.

* Returns users IDs instead of User.

* Removes non-essential group data.

* MM-11807: Add GroupFilter to LDAP config. (#9513)

* MM-11807: Add GroupFilter to LDAP config.

* Add diagnostic.

* Adds new config option for using 'memberOf' overlay.

* Adds API endpoint to link a group.

* Removes debug statements.

* Adds unlink group API endpoint.

* Fix to LDAP API. Adds API method to client4 and app.

* Adds some missing app methods. Renames API unexported func.

* Fixes link/unlink API path to accept valid DNs.

* Allow any character for DN portion of path.

* Switches from DN to objectGUID or entryUUID as the remote identifier linking LDAP groups to MM groups.

* Formatting.

* Formatting.

* Setting group name field to an ID for phase 1.

* Adds an LDAP config field to Setting up configuration for local LDAP.

* Changes to LDAP and GroupStore interfaces.

* Draft of nesting groups in API response.

* Removes unnecessary tree models.

* Updates group membershipt create store method to also restore.

* Adds new config to test config.

* Accept AD format length.

* Switches to SetUniqueTogether method.

* Updates revert.

* Tweaks to syncing queries .

* Updates query for pending team and channel memberships.

* Removes old GroupSyncableScanner usage. Some formatting and renaming.

* Fixes bug setting syncable type in selecting paged.

* Adds tests for syncables populator.

* Only add users to teams and channels that are not deleted.

* Renames method.

* Updates test LDAP setup.

* Removes memberof config stuff.

* Renames.

* Updates test data.

* Fix for gofmt.

* Adds missing license.

* Adds missing teardowns.

* Test fix.

* Adds a cycle to the groups test data.

* Changes API to return flat list.

* Removes some unused interface and app methods.

* Returns empty braces if results are empty.

* Adds more LDAP test data.

* Fix for test data error.

* Adds error.

* Moves test groups.

* Adds OU for load test data.

* Moves load test ou creation to load data.

* Adds a new bool flag to SCIMGroups.

* Removes SCIMGroup completely.

* Removes FULL JOIN because it is not supported in MySQL.

* Adds tests for sync queries; renames constant.

* Bad merge fix.

* Vet fix.

* Returning OK on delete ldap group link

* Removes foreign key constraints.

* Adding total to the ldap getAllGroups api endpoint

* Adds get group members page.

* Removes pagination from groups syncables list API.

* Adding syncable check now that foreign key constraint is removes.

* Joins teams and channels to group syncables.

* Adds group member count.

* Adding GetAllChannels and SearchAllChannels for system admins only

* Fix.

* Test fix from pagination removal.

* Orders groupmembers by createat.

* Fixing search of all channels

* Test fix after removing pagination.

* JSON syntax error fix.

* Changing tests (for now) pending investigation.

* Adding GetAllChannels and SearchAllChannels tests for the store

* Adding GetAllChannels and SearchAllChannels API tests

* Omit empty JSON values of group syncables.

* Fixing GetAllChannels and SearchAllChannels tests

* Fixing GetAllChannels and SearchAllChannels store tests

* Fixing GetAllChannels api tests

* Adds 'LDAP groups' feature flag. (#9861)

* Migrate new client functions to idiomatic error handling

* Test fixes.

* Simplification of groups api (#9860)

* Simplification of groups api

* Fixing RequireSyncableType

* Test fix.

* Update api4/group.go

Co-Authored-By: mkraft <martinkraft@gmail.com>

* Update api4/group.go

Co-Authored-By: mkraft <martinkraft@gmail.com>

* Update api4/group.go

Co-Authored-By: mkraft <martinkraft@gmail.com>

* Update api4/group.go

Co-Authored-By: mkraft <martinkraft@gmail.com>

* Update api4/group.go

Co-Authored-By: mkraft <martinkraft@gmail.com>

* Update api4/group.go

Co-Authored-By: mkraft <martinkraft@gmail.com>

* Update api4/group.go

Co-Authored-By: mkraft <martinkraft@gmail.com>

* Update api4/group.go

Co-Authored-By: mkraft <martinkraft@gmail.com>

* Update api4/group.go

Co-Authored-By: mkraft <martinkraft@gmail.com>

* Update api4/group.go

Co-Authored-By: mkraft <martinkraft@gmail.com>

* Fix copy/paste error.

* Fix copy/paste error.

* Adds missing return, changes to correct HTTP status code.

* Adds missing return, changes status codes.

* Check for license.

* Renames variable for new signature.

* Adds client method to get a group.

* Adds client method and tests for PatchGroup.

* Adds more API tests.

* Adds groups API tests.

* Adds client method and tests for getting group syncables.

* Adds tests for patching group teams and channels.

* Update to translations.

* Removes test.

* Fix incorrect conditional.

* Removes unnecessary nil check.

* Removes unnecessary return.

* Updates comment, removes unused variable.

* Uses consistent JSON unmarshal pattern.

* Uses consistent JSON unmarshal pattern.

* Moves const block.

* Switches 'already linked' from error to success response.

* Removes commented-out code.

* Switched to status ok.

* Add parens for readability.

* Fix copy/paste error.

* Unexport some structs.

* Removes repeated validity check.

* Return without attempting commit if there's a rollback.

* Fix incorrect HTTP status code.

* Update store/sqlstore/group_supplier.go

Co-Authored-By: mkraft <martinkraft@gmail.com>

* Adds utility methods for going from groupsyncable to groupteam and groupchannel.

* Fixing george suggestions (#9911)

* Test fix.

* Adds QA data to VC with visualization.

* Fixes typo in graph image.

* Update display name when re-linking in case it has changed in LDAP.

* Adds ability to configure group display name and unique identifier. (#9923)

* Adds ability to configure group display name and unique identifier.

* Adds some configs to confi-ldap make command.

* Fix for move of session.

* Exposes method for use by SAML package.

* Switches GroupSyncableType from int to string.

* Update Jenkins build files.

* Removes unused variable assignment.

* Removes old unnecessary early return.

* Removes unnecessary variable.

* Moves param parsing before license and permissions checks.

* Removes old code.

* Compares agains underlying error rather than error id.

* Switches tests to assertions.

* Adds more assertions.

* Adds missing return.

* Adds space after comma for added legibility.

* Moves a view model to the api package.

* Unexports method.

* Uses id validator function.

* Fix docker-compose flag.

* Typo fix.

* Moves index creation to supplier.

* Removes bad merge.

* Renames parameter.

* Re-adds space.

* Removes unnecessary transaction.

* Escapes the Groups table name with backticks because it is a reserved keyword.

* Fix roles cache bug

* Removing unnecesiary deserializing function

* Switches table name rather than custom SQL everywhere for Postgres without backticks.

* Removes redundant check for sql.ErrNoRows.

* Removes redundant check for sql.ErrNoRows.

* Removes data integrity check and redundant nil conditional.

* Removes redundant check for sql.ErrNoRows.

* Removes unnecessary query.

* Removes ID length validation from persistence tier.

* Makes some supplier methods idempotent.

* Removes some empty switch defaults.

* Renames Group Type field to Source.

* Fix for mistaken field name change.

* Uses IsValidId function.

* Removes comment.

* Changes json key name.

* Removes test because no longer validating user.

* Moves model state validation to app layer.

* Don't create Groups.CanLeave column until phase 2.

* Removes state validation until properties are used in phase 2.

* Removes duplicated check.

* Removes state validation until properties are used in phase 2.

* Removes some tests until phase 2.

* Comment-out a bunch of test related to CanLeave.

* Extra unmarshal validation check. Removes more code for CanLeave.

* Removes tests for CanLeave.

* Explict error msg.

* Rewrite queries.

* Changes index name. Adds index.

* Removes assertion.

* Adds experimental feature flag.
2019-01-10 15:17:31 -05:00

566 lines
12 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package web
import (
"net/http"
"path"
"regexp"
"strings"
"github.com/mattermost/mattermost-server/app"
"github.com/mattermost/mattermost-server/mlog"
"github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/utils"
)
type Context struct {
App *app.App
Log *mlog.Logger
Params *Params
Err *model.AppError
siteURLHeader string
}
func (c *Context) LogAudit(extraInfo string) {
audit := &model.Audit{UserId: c.App.Session.UserId, IpAddress: c.App.IpAddress, Action: c.App.Path, ExtraInfo: extraInfo, SessionId: c.App.Session.Id}
if r := <-c.App.Srv.Store.Audit().Save(audit); r.Err != nil {
c.LogError(r.Err)
}
}
func (c *Context) LogAuditWithUserId(userId, extraInfo string) {
if len(c.App.Session.UserId) > 0 {
extraInfo = strings.TrimSpace(extraInfo + " session_user=" + c.App.Session.UserId)
}
audit := &model.Audit{UserId: userId, IpAddress: c.App.IpAddress, Action: c.App.Path, ExtraInfo: extraInfo, SessionId: c.App.Session.Id}
if r := <-c.App.Srv.Store.Audit().Save(audit); r.Err != nil {
c.LogError(r.Err)
}
}
func (c *Context) LogError(err *model.AppError) {
// Filter out 404s, endless reconnects and browser compatibility errors
if err.StatusCode == http.StatusNotFound ||
(c.App.Path == "/api/v3/users/websocket" && err.StatusCode == http.StatusUnauthorized) ||
err.Id == "web.check_browser_compatibility.app_error" {
c.LogDebug(err)
} else {
c.Log.Error(
err.SystemMessage(utils.TDefault),
mlog.String("err_where", err.Where),
mlog.Int("http_code", err.StatusCode),
mlog.String("err_details", err.DetailedError),
)
}
}
func (c *Context) LogInfo(err *model.AppError) {
// Filter out 401s
if err.StatusCode == http.StatusUnauthorized {
c.LogDebug(err)
} else {
c.Log.Info(
err.SystemMessage(utils.TDefault),
mlog.String("err_where", err.Where),
mlog.Int("http_code", err.StatusCode),
mlog.String("err_details", err.DetailedError),
)
}
}
func (c *Context) LogDebug(err *model.AppError) {
c.Log.Debug(
err.SystemMessage(utils.TDefault),
mlog.String("err_where", err.Where),
mlog.Int("http_code", err.StatusCode),
mlog.String("err_details", err.DetailedError),
)
}
func (c *Context) IsSystemAdmin() bool {
return c.App.SessionHasPermissionTo(c.App.Session, model.PERMISSION_MANAGE_SYSTEM)
}
func (c *Context) SessionRequired() {
if !*c.App.Config().ServiceSettings.EnableUserAccessTokens && c.App.Session.Props[model.SESSION_PROP_TYPE] == model.SESSION_TYPE_USER_ACCESS_TOKEN {
c.Err = model.NewAppError("", "api.context.session_expired.app_error", nil, "UserAccessToken", http.StatusUnauthorized)
return
}
if len(c.App.Session.UserId) == 0 {
c.Err = model.NewAppError("", "api.context.session_expired.app_error", nil, "UserRequired", http.StatusUnauthorized)
return
}
}
func (c *Context) MfaRequired() {
// Must be licensed for MFA and have it configured for enforcement
if license := c.App.License(); license == nil || !*license.Features.MFA || !*c.App.Config().ServiceSettings.EnableMultifactorAuthentication || !*c.App.Config().ServiceSettings.EnforceMultifactorAuthentication {
return
}
// OAuth integrations are excepted
if c.App.Session.IsOAuth {
return
}
if user, err := c.App.GetUser(c.App.Session.UserId); err != nil {
c.Err = model.NewAppError("", "api.context.session_expired.app_error", nil, "MfaRequired", http.StatusUnauthorized)
return
} else {
// Only required for email and ldap accounts
if user.AuthService != "" &&
user.AuthService != model.USER_AUTH_SERVICE_EMAIL &&
user.AuthService != model.USER_AUTH_SERVICE_LDAP {
return
}
// Special case to let user get themself
subpath, _ := utils.GetSubpathFromConfig(c.App.Config())
if c.App.Path == path.Join(subpath, "/api/v4/users/me") {
return
}
if !user.MfaActive {
c.Err = model.NewAppError("", "api.context.mfa_required.app_error", nil, "MfaRequired", http.StatusForbidden)
return
}
}
}
func (c *Context) RemoveSessionCookie(w http.ResponseWriter, r *http.Request) {
cookie := &http.Cookie{
Name: model.SESSION_COOKIE_TOKEN,
Value: "",
Path: "/",
MaxAge: -1,
HttpOnly: true,
}
http.SetCookie(w, cookie)
}
func (c *Context) SetInvalidParam(parameter string) {
c.Err = NewInvalidParamError(parameter)
}
func (c *Context) SetInvalidUrlParam(parameter string) {
c.Err = NewInvalidUrlParamError(parameter)
}
func (c *Context) HandleEtag(etag string, routeName string, w http.ResponseWriter, r *http.Request) bool {
metrics := c.App.Metrics
if et := r.Header.Get(model.HEADER_ETAG_CLIENT); len(etag) > 0 {
if et == etag {
w.Header().Set(model.HEADER_ETAG_SERVER, etag)
w.WriteHeader(http.StatusNotModified)
if metrics != nil {
metrics.IncrementEtagHitCounter(routeName)
}
return true
}
}
if metrics != nil {
metrics.IncrementEtagMissCounter(routeName)
}
return false
}
func NewInvalidParamError(parameter string) *model.AppError {
err := model.NewAppError("Context", "api.context.invalid_body_param.app_error", map[string]interface{}{"Name": parameter}, "", http.StatusBadRequest)
return err
}
func NewInvalidUrlParamError(parameter string) *model.AppError {
err := model.NewAppError("Context", "api.context.invalid_url_param.app_error", map[string]interface{}{"Name": parameter}, "", http.StatusBadRequest)
return err
}
func (c *Context) SetPermissionError(permission *model.Permission) {
c.Err = model.NewAppError("Permissions", "api.context.permissions.app_error", nil, "userId="+c.App.Session.UserId+", "+"permission="+permission.Id, http.StatusForbidden)
}
func (c *Context) SetSiteURLHeader(url string) {
c.siteURLHeader = strings.TrimRight(url, "/")
}
func (c *Context) GetSiteURLHeader() string {
return c.siteURLHeader
}
func (c *Context) RequireUserId() *Context {
if c.Err != nil {
return c
}
if c.Params.UserId == model.ME {
c.Params.UserId = c.App.Session.UserId
}
if len(c.Params.UserId) != 26 {
c.SetInvalidUrlParam("user_id")
}
return c
}
func (c *Context) RequireTeamId() *Context {
if c.Err != nil {
return c
}
if len(c.Params.TeamId) != 26 {
c.SetInvalidUrlParam("team_id")
}
return c
}
func (c *Context) RequireInviteId() *Context {
if c.Err != nil {
return c
}
if len(c.Params.InviteId) == 0 {
c.SetInvalidUrlParam("invite_id")
}
return c
}
func (c *Context) RequireTokenId() *Context {
if c.Err != nil {
return c
}
if len(c.Params.TokenId) != 26 {
c.SetInvalidUrlParam("token_id")
}
return c
}
func (c *Context) RequireChannelId() *Context {
if c.Err != nil {
return c
}
if len(c.Params.ChannelId) != 26 {
c.SetInvalidUrlParam("channel_id")
}
return c
}
func (c *Context) RequireUsername() *Context {
if c.Err != nil {
return c
}
if !model.IsValidUsername(c.Params.Username) {
c.SetInvalidParam("username")
}
return c
}
func (c *Context) RequirePostId() *Context {
if c.Err != nil {
return c
}
if len(c.Params.PostId) != 26 {
c.SetInvalidUrlParam("post_id")
}
return c
}
func (c *Context) RequireAppId() *Context {
if c.Err != nil {
return c
}
if len(c.Params.AppId) != 26 {
c.SetInvalidUrlParam("app_id")
}
return c
}
func (c *Context) RequireFileId() *Context {
if c.Err != nil {
return c
}
if len(c.Params.FileId) != 26 {
c.SetInvalidUrlParam("file_id")
}
return c
}
func (c *Context) RequireFilename() *Context {
if c.Err != nil {
return c
}
if len(c.Params.Filename) == 0 {
c.SetInvalidUrlParam("filename")
}
return c
}
func (c *Context) RequirePluginId() *Context {
if c.Err != nil {
return c
}
if len(c.Params.PluginId) == 0 {
c.SetInvalidUrlParam("plugin_id")
}
return c
}
func (c *Context) RequireReportId() *Context {
if c.Err != nil {
return c
}
if len(c.Params.ReportId) != 26 {
c.SetInvalidUrlParam("report_id")
}
return c
}
func (c *Context) RequireEmojiId() *Context {
if c.Err != nil {
return c
}
if len(c.Params.EmojiId) != 26 {
c.SetInvalidUrlParam("emoji_id")
}
return c
}
func (c *Context) RequireTeamName() *Context {
if c.Err != nil {
return c
}
if !model.IsValidTeamName(c.Params.TeamName) {
c.SetInvalidUrlParam("team_name")
}
return c
}
func (c *Context) RequireChannelName() *Context {
if c.Err != nil {
return c
}
if !model.IsValidChannelIdentifier(c.Params.ChannelName) {
c.SetInvalidUrlParam("channel_name")
}
return c
}
func (c *Context) RequireEmail() *Context {
if c.Err != nil {
return c
}
if !model.IsValidEmail(c.Params.Email) {
c.SetInvalidUrlParam("email")
}
return c
}
func (c *Context) RequireCategory() *Context {
if c.Err != nil {
return c
}
if !model.IsValidAlphaNumHyphenUnderscore(c.Params.Category, true) {
c.SetInvalidUrlParam("category")
}
return c
}
func (c *Context) RequireService() *Context {
if c.Err != nil {
return c
}
if len(c.Params.Service) == 0 {
c.SetInvalidUrlParam("service")
}
return c
}
func (c *Context) RequirePreferenceName() *Context {
if c.Err != nil {
return c
}
if !model.IsValidAlphaNumHyphenUnderscore(c.Params.PreferenceName, true) {
c.SetInvalidUrlParam("preference_name")
}
return c
}
func (c *Context) RequireEmojiName() *Context {
if c.Err != nil {
return c
}
validName := regexp.MustCompile(`^[a-zA-Z0-9\-\+_]+$`)
if len(c.Params.EmojiName) == 0 || len(c.Params.EmojiName) > model.EMOJI_NAME_MAX_LENGTH || !validName.MatchString(c.Params.EmojiName) {
c.SetInvalidUrlParam("emoji_name")
}
return c
}
func (c *Context) RequireHookId() *Context {
if c.Err != nil {
return c
}
if len(c.Params.HookId) != 26 {
c.SetInvalidUrlParam("hook_id")
}
return c
}
func (c *Context) RequireCommandId() *Context {
if c.Err != nil {
return c
}
if len(c.Params.CommandId) != 26 {
c.SetInvalidUrlParam("command_id")
}
return c
}
func (c *Context) RequireJobId() *Context {
if c.Err != nil {
return c
}
if len(c.Params.JobId) != 26 {
c.SetInvalidUrlParam("job_id")
}
return c
}
func (c *Context) RequireJobType() *Context {
if c.Err != nil {
return c
}
if len(c.Params.JobType) == 0 || len(c.Params.JobType) > 32 {
c.SetInvalidUrlParam("job_type")
}
return c
}
func (c *Context) RequireActionId() *Context {
if c.Err != nil {
return c
}
if len(c.Params.ActionId) != 26 {
c.SetInvalidUrlParam("action_id")
}
return c
}
func (c *Context) RequireRoleId() *Context {
if c.Err != nil {
return c
}
if len(c.Params.RoleId) != 26 {
c.SetInvalidUrlParam("role_id")
}
return c
}
func (c *Context) RequireSchemeId() *Context {
if c.Err != nil {
return c
}
if len(c.Params.SchemeId) != 26 {
c.SetInvalidUrlParam("scheme_id")
}
return c
}
func (c *Context) RequireRoleName() *Context {
if c.Err != nil {
return c
}
if !model.IsValidRoleName(c.Params.RoleName) {
c.SetInvalidUrlParam("role_name")
}
return c
}
func (c *Context) RequireGroupId() *Context {
if c.Err != nil {
return c
}
if len(c.Params.GroupId) != 26 {
c.SetInvalidUrlParam("group_id")
}
return c
}
func (c *Context) RequireRemoteId() *Context {
if c.Err != nil {
return c
}
if len(c.Params.RemoteId) == 0 {
c.SetInvalidUrlParam("remote_id")
}
return c
}
func (c *Context) RequireSyncableId() *Context {
if c.Err != nil {
return c
}
if len(c.Params.SyncableId) != 26 {
c.SetInvalidUrlParam("syncable_id")
}
return c
}
func (c *Context) RequireSyncableType() *Context {
if c.Err != nil {
return c
}
if c.Params.SyncableType != model.GroupSyncableTypeTeam && c.Params.SyncableType != model.GroupSyncableTypeChannel {
c.SetInvalidUrlParam("syncable_type")
}
return c
}