1// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2// See LICENSE.txt for license information.
3
4package web
5
6import (
7	"net/http"
8	"path"
9	"regexp"
10	"strings"
11
12	"github.com/mattermost/mattermost-server/v6/app"
13	"github.com/mattermost/mattermost-server/v6/app/request"
14	"github.com/mattermost/mattermost-server/v6/audit"
15	"github.com/mattermost/mattermost-server/v6/model"
16	"github.com/mattermost/mattermost-server/v6/shared/i18n"
17	"github.com/mattermost/mattermost-server/v6/shared/mlog"
18	"github.com/mattermost/mattermost-server/v6/utils"
19)
20
21type Context struct {
22	App           app.AppIface
23	AppContext    *request.Context
24	Logger        *mlog.Logger
25	Params        *Params
26	Err           *model.AppError
27	siteURLHeader string
28}
29
30// LogAuditRec logs an audit record using default LevelAPI.
31func (c *Context) LogAuditRec(rec *audit.Record) {
32	c.LogAuditRecWithLevel(rec, app.LevelAPI)
33}
34
35// LogAuditRec logs an audit record using specified Level.
36// If the context is flagged with a permissions error then `level`
37// is ignored and the audit record is emitted with `LevelPerms`.
38func (c *Context) LogAuditRecWithLevel(rec *audit.Record, level mlog.Level) {
39	if rec == nil {
40		return
41	}
42	if c.Err != nil {
43		rec.AddMeta("err", c.Err.Id)
44		rec.AddMeta("code", c.Err.StatusCode)
45		if c.Err.Id == "api.context.permissions.app_error" {
46			level = app.LevelPerms
47		}
48		rec.Fail()
49	}
50	c.App.Srv().Audit.LogRecord(level, *rec)
51}
52
53// MakeAuditRecord creates a audit record pre-populated with data from this context.
54func (c *Context) MakeAuditRecord(event string, initialStatus string) *audit.Record {
55	rec := &audit.Record{
56		APIPath:   c.AppContext.Path(),
57		Event:     event,
58		Status:    initialStatus,
59		UserID:    c.AppContext.Session().UserId,
60		SessionID: c.AppContext.Session().Id,
61		Client:    c.AppContext.UserAgent(),
62		IPAddress: c.AppContext.IPAddress(),
63		Meta:      audit.Meta{audit.KeyClusterID: c.App.GetClusterId()},
64	}
65	rec.AddMetaTypeConverter(model.AuditModelTypeConv)
66
67	return rec
68}
69
70func (c *Context) LogAudit(extraInfo string) {
71	audit := &model.Audit{UserId: c.AppContext.Session().UserId, IpAddress: c.AppContext.IPAddress(), Action: c.AppContext.Path(), ExtraInfo: extraInfo, SessionId: c.AppContext.Session().Id}
72	if err := c.App.Srv().Store.Audit().Save(audit); err != nil {
73		appErr := model.NewAppError("LogAudit", "app.audit.save.saving.app_error", nil, err.Error(), http.StatusInternalServerError)
74		c.LogErrorByCode(appErr)
75	}
76}
77
78func (c *Context) LogAuditWithUserId(userId, extraInfo string) {
79	if c.AppContext.Session().UserId != "" {
80		extraInfo = strings.TrimSpace(extraInfo + " session_user=" + c.AppContext.Session().UserId)
81	}
82
83	audit := &model.Audit{UserId: userId, IpAddress: c.AppContext.IPAddress(), Action: c.AppContext.Path(), ExtraInfo: extraInfo, SessionId: c.AppContext.Session().Id}
84	if err := c.App.Srv().Store.Audit().Save(audit); err != nil {
85		appErr := model.NewAppError("LogAuditWithUserId", "app.audit.save.saving.app_error", nil, err.Error(), http.StatusInternalServerError)
86		c.LogErrorByCode(appErr)
87	}
88}
89
90func (c *Context) LogErrorByCode(err *model.AppError) {
91	code := err.StatusCode
92	msg := err.SystemMessage(i18n.TDefault)
93	fields := []mlog.Field{
94		mlog.String("err_where", err.Where),
95		mlog.Int("http_code", err.StatusCode),
96		mlog.String("err_details", err.DetailedError),
97	}
98	switch {
99	case (code >= http.StatusBadRequest && code < http.StatusInternalServerError) ||
100		err.Id == "web.check_browser_compatibility.app_error":
101		c.Logger.Debug(msg, fields...)
102	case code == http.StatusNotImplemented:
103		c.Logger.Info(msg, fields...)
104	default:
105		c.Logger.Error(msg, fields...)
106	}
107}
108
109func (c *Context) IsSystemAdmin() bool {
110	return c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem)
111}
112
113func (c *Context) SessionRequired() {
114	if !*c.App.Config().ServiceSettings.EnableUserAccessTokens &&
115		c.AppContext.Session().Props[model.SessionPropType] == model.SessionTypeUserAccessToken &&
116		c.AppContext.Session().Props[model.SessionPropIsBot] != model.SessionPropIsBotValue {
117
118		c.Err = model.NewAppError("", "api.context.session_expired.app_error", nil, "UserAccessToken", http.StatusUnauthorized)
119		return
120	}
121
122	if c.AppContext.Session().UserId == "" {
123		c.Err = model.NewAppError("", "api.context.session_expired.app_error", nil, "UserRequired", http.StatusUnauthorized)
124		return
125	}
126}
127
128func (c *Context) CloudKeyRequired() {
129	if license := c.App.Srv().License(); license == nil || !*license.Features.Cloud || c.AppContext.Session().Props[model.SessionPropType] != model.SessionTypeCloudKey {
130		c.Err = model.NewAppError("", "api.context.session_expired.app_error", nil, "TokenRequired", http.StatusUnauthorized)
131		return
132	}
133}
134
135func (c *Context) RemoteClusterTokenRequired() {
136	if license := c.App.Srv().License(); license == nil || !*license.Features.RemoteClusterService || c.AppContext.Session().Props[model.SessionPropType] != model.SessionTypeRemoteclusterToken {
137		c.Err = model.NewAppError("", "api.context.session_expired.app_error", nil, "TokenRequired", http.StatusUnauthorized)
138		return
139	}
140}
141
142func (c *Context) MfaRequired() {
143	// Must be licensed for MFA and have it configured for enforcement
144	if license := c.App.Srv().License(); license == nil || !*license.Features.MFA || !*c.App.Config().ServiceSettings.EnableMultifactorAuthentication || !*c.App.Config().ServiceSettings.EnforceMultifactorAuthentication {
145		return
146	}
147
148	// OAuth integrations are excepted
149	if c.AppContext.Session().IsOAuth {
150		return
151	}
152
153	user, err := c.App.GetUser(c.AppContext.Session().UserId)
154	if err != nil {
155		c.Err = model.NewAppError("MfaRequired", "api.context.get_user.app_error", nil, err.Error(), http.StatusUnauthorized)
156		return
157	}
158
159	if user.IsGuest() && !*c.App.Config().GuestAccountsSettings.EnforceMultifactorAuthentication {
160		return
161	}
162	// Only required for email and ldap accounts
163	if user.AuthService != "" &&
164		user.AuthService != model.UserAuthServiceEmail &&
165		user.AuthService != model.UserAuthServiceLdap {
166		return
167	}
168
169	// Special case to let user get themself
170	subpath, _ := utils.GetSubpathFromConfig(c.App.Config())
171	if c.AppContext.Path() == path.Join(subpath, "/api/v4/users/me") {
172		return
173	}
174
175	// Bots are exempt
176	if user.IsBot {
177		return
178	}
179
180	if !user.MfaActive {
181		c.Err = model.NewAppError("MfaRequired", "api.context.mfa_required.app_error", nil, "", http.StatusForbidden)
182		return
183	}
184}
185
186// ExtendSessionExpiryIfNeeded will update Session.ExpiresAt based on session lengths in config.
187// Session cookies will be resent to the client with updated max age.
188func (c *Context) ExtendSessionExpiryIfNeeded(w http.ResponseWriter, r *http.Request) {
189	if ok := c.App.ExtendSessionExpiryIfNeeded(c.AppContext.Session()); ok {
190		c.App.AttachSessionCookies(c.AppContext, w, r)
191	}
192}
193
194func (c *Context) RemoveSessionCookie(w http.ResponseWriter, r *http.Request) {
195	subpath, _ := utils.GetSubpathFromConfig(c.App.Config())
196
197	cookie := &http.Cookie{
198		Name:     model.SessionCookieToken,
199		Value:    "",
200		Path:     subpath,
201		MaxAge:   -1,
202		HttpOnly: true,
203	}
204
205	http.SetCookie(w, cookie)
206}
207
208func (c *Context) SetInvalidParam(parameter string) {
209	c.Err = NewInvalidParamError(parameter)
210}
211
212func (c *Context) SetInvalidURLParam(parameter string) {
213	c.Err = NewInvalidURLParamError(parameter)
214}
215
216func (c *Context) SetServerBusyError() {
217	c.Err = NewServerBusyError()
218}
219
220func (c *Context) SetInvalidRemoteIdError(id string) {
221	c.Err = NewInvalidRemoteIdError(id)
222}
223
224func (c *Context) SetInvalidRemoteClusterTokenError() {
225	c.Err = NewInvalidRemoteClusterTokenError()
226}
227
228func (c *Context) SetJSONEncodingError() {
229	c.Err = NewJSONEncodingError()
230}
231
232func (c *Context) SetCommandNotFoundError() {
233	c.Err = model.NewAppError("GetCommand", "store.sql_command.save.get.app_error", nil, "", http.StatusNotFound)
234}
235
236func (c *Context) HandleEtag(etag string, routeName string, w http.ResponseWriter, r *http.Request) bool {
237	metrics := c.App.Metrics()
238	if et := r.Header.Get(model.HeaderEtagClient); etag != "" {
239		if et == etag {
240			w.Header().Set(model.HeaderEtagServer, etag)
241			w.WriteHeader(http.StatusNotModified)
242			if metrics != nil {
243				metrics.IncrementEtagHitCounter(routeName)
244			}
245			return true
246		}
247	}
248
249	if metrics != nil {
250		metrics.IncrementEtagMissCounter(routeName)
251	}
252
253	return false
254}
255
256func NewInvalidParamError(parameter string) *model.AppError {
257	err := model.NewAppError("Context", "api.context.invalid_body_param.app_error", map[string]interface{}{"Name": parameter}, "", http.StatusBadRequest)
258	return err
259}
260func NewInvalidURLParamError(parameter string) *model.AppError {
261	err := model.NewAppError("Context", "api.context.invalid_url_param.app_error", map[string]interface{}{"Name": parameter}, "", http.StatusBadRequest)
262	return err
263}
264func NewServerBusyError() *model.AppError {
265	err := model.NewAppError("Context", "api.context.server_busy.app_error", nil, "", http.StatusServiceUnavailable)
266	return err
267}
268
269func NewInvalidRemoteIdError(parameter string) *model.AppError {
270	err := model.NewAppError("Context", "api.context.remote_id_invalid.app_error", map[string]interface{}{"RemoteId": parameter}, "", http.StatusBadRequest)
271	return err
272}
273
274func NewInvalidRemoteClusterTokenError() *model.AppError {
275	err := model.NewAppError("Context", "api.context.remote_id_invalid.app_error", nil, "", http.StatusUnauthorized)
276	return err
277}
278
279func NewJSONEncodingError() *model.AppError {
280	err := model.NewAppError("Context", "api.context.json_encoding.app_error", nil, "", http.StatusInternalServerError)
281	return err
282}
283
284func (c *Context) SetPermissionError(permissions ...*model.Permission) {
285	c.Err = c.App.MakePermissionError(c.AppContext.Session(), permissions)
286}
287
288func (c *Context) SetSiteURLHeader(url string) {
289	c.siteURLHeader = strings.TrimRight(url, "/")
290}
291
292func (c *Context) GetSiteURLHeader() string {
293	return c.siteURLHeader
294}
295
296func (c *Context) RequireUserId() *Context {
297	if c.Err != nil {
298		return c
299	}
300
301	if c.Params.UserId == model.Me {
302		c.Params.UserId = c.AppContext.Session().UserId
303	}
304
305	if !model.IsValidId(c.Params.UserId) {
306		c.SetInvalidURLParam("user_id")
307	}
308	return c
309}
310
311func (c *Context) RequireTeamId() *Context {
312	if c.Err != nil {
313		return c
314	}
315
316	if !model.IsValidId(c.Params.TeamId) {
317		c.SetInvalidURLParam("team_id")
318	}
319	return c
320}
321
322func (c *Context) RequireCategoryId() *Context {
323	if c.Err != nil {
324		return c
325	}
326
327	if !model.IsValidCategoryId(c.Params.CategoryId) {
328		c.SetInvalidURLParam("category_id")
329	}
330	return c
331}
332
333func (c *Context) RequireInviteId() *Context {
334	if c.Err != nil {
335		return c
336	}
337
338	if c.Params.InviteId == "" {
339		c.SetInvalidURLParam("invite_id")
340	}
341	return c
342}
343
344func (c *Context) RequireTokenId() *Context {
345	if c.Err != nil {
346		return c
347	}
348
349	if !model.IsValidId(c.Params.TokenId) {
350		c.SetInvalidURLParam("token_id")
351	}
352	return c
353}
354
355func (c *Context) RequireThreadId() *Context {
356	if c.Err != nil {
357		return c
358	}
359
360	if !model.IsValidId(c.Params.ThreadId) {
361		c.SetInvalidURLParam("thread_id")
362	}
363	return c
364}
365
366func (c *Context) RequireTimestamp() *Context {
367	if c.Err != nil {
368		return c
369	}
370
371	if c.Params.Timestamp == 0 {
372		c.SetInvalidURLParam("timestamp")
373	}
374	return c
375}
376
377func (c *Context) RequireChannelId() *Context {
378	if c.Err != nil {
379		return c
380	}
381
382	if !model.IsValidId(c.Params.ChannelId) {
383		c.SetInvalidURLParam("channel_id")
384	}
385	return c
386}
387
388func (c *Context) RequireUsername() *Context {
389	if c.Err != nil {
390		return c
391	}
392
393	if !model.IsValidUsername(c.Params.Username) {
394		c.SetInvalidParam("username")
395	}
396
397	return c
398}
399
400func (c *Context) RequirePostId() *Context {
401	if c.Err != nil {
402		return c
403	}
404
405	if !model.IsValidId(c.Params.PostId) {
406		c.SetInvalidURLParam("post_id")
407	}
408	return c
409}
410
411func (c *Context) RequirePolicyId() *Context {
412	if c.Err != nil {
413		return c
414	}
415
416	if !model.IsValidId(c.Params.PolicyId) {
417		c.SetInvalidURLParam("policy_id")
418	}
419	return c
420}
421
422func (c *Context) RequireAppId() *Context {
423	if c.Err != nil {
424		return c
425	}
426
427	if !model.IsValidId(c.Params.AppId) {
428		c.SetInvalidURLParam("app_id")
429	}
430	return c
431}
432
433func (c *Context) RequireFileId() *Context {
434	if c.Err != nil {
435		return c
436	}
437
438	if !model.IsValidId(c.Params.FileId) {
439		c.SetInvalidURLParam("file_id")
440	}
441
442	return c
443}
444
445func (c *Context) RequireUploadId() *Context {
446	if c.Err != nil {
447		return c
448	}
449
450	if !model.IsValidId(c.Params.UploadId) {
451		c.SetInvalidURLParam("upload_id")
452	}
453
454	return c
455}
456
457func (c *Context) RequireFilename() *Context {
458	if c.Err != nil {
459		return c
460	}
461
462	if c.Params.Filename == "" {
463		c.SetInvalidURLParam("filename")
464	}
465
466	return c
467}
468
469func (c *Context) RequirePluginId() *Context {
470	if c.Err != nil {
471		return c
472	}
473
474	if c.Params.PluginId == "" {
475		c.SetInvalidURLParam("plugin_id")
476	}
477
478	return c
479}
480
481func (c *Context) RequireReportId() *Context {
482	if c.Err != nil {
483		return c
484	}
485
486	if !model.IsValidId(c.Params.ReportId) {
487		c.SetInvalidURLParam("report_id")
488	}
489	return c
490}
491
492func (c *Context) RequireEmojiId() *Context {
493	if c.Err != nil {
494		return c
495	}
496
497	if !model.IsValidId(c.Params.EmojiId) {
498		c.SetInvalidURLParam("emoji_id")
499	}
500	return c
501}
502
503func (c *Context) RequireTeamName() *Context {
504	if c.Err != nil {
505		return c
506	}
507
508	if !model.IsValidTeamName(c.Params.TeamName) {
509		c.SetInvalidURLParam("team_name")
510	}
511
512	return c
513}
514
515func (c *Context) RequireChannelName() *Context {
516	if c.Err != nil {
517		return c
518	}
519
520	if !model.IsValidChannelIdentifier(c.Params.ChannelName) {
521		c.SetInvalidURLParam("channel_name")
522	}
523
524	return c
525}
526
527func (c *Context) SanitizeEmail() *Context {
528	if c.Err != nil {
529		return c
530	}
531	c.Params.Email = strings.ToLower(c.Params.Email)
532	if !model.IsValidEmail(c.Params.Email) {
533		c.SetInvalidURLParam("email")
534	}
535
536	return c
537}
538
539func (c *Context) RequireCategory() *Context {
540	if c.Err != nil {
541		return c
542	}
543
544	if !model.IsValidAlphaNumHyphenUnderscore(c.Params.Category, true) {
545		c.SetInvalidURLParam("category")
546	}
547
548	return c
549}
550
551func (c *Context) RequireService() *Context {
552	if c.Err != nil {
553		return c
554	}
555
556	if c.Params.Service == "" {
557		c.SetInvalidURLParam("service")
558	}
559
560	return c
561}
562
563func (c *Context) RequirePreferenceName() *Context {
564	if c.Err != nil {
565		return c
566	}
567
568	if !model.IsValidAlphaNumHyphenUnderscore(c.Params.PreferenceName, true) {
569		c.SetInvalidURLParam("preference_name")
570	}
571
572	return c
573}
574
575func (c *Context) RequireEmojiName() *Context {
576	if c.Err != nil {
577		return c
578	}
579
580	validName := regexp.MustCompile(`^[a-zA-Z0-9\-\+_]+$`)
581
582	if c.Params.EmojiName == "" || len(c.Params.EmojiName) > model.EmojiNameMaxLength || !validName.MatchString(c.Params.EmojiName) {
583		c.SetInvalidURLParam("emoji_name")
584	}
585
586	return c
587}
588
589func (c *Context) RequireHookId() *Context {
590	if c.Err != nil {
591		return c
592	}
593
594	if !model.IsValidId(c.Params.HookId) {
595		c.SetInvalidURLParam("hook_id")
596	}
597
598	return c
599}
600
601func (c *Context) RequireCommandId() *Context {
602	if c.Err != nil {
603		return c
604	}
605
606	if !model.IsValidId(c.Params.CommandId) {
607		c.SetInvalidURLParam("command_id")
608	}
609	return c
610}
611
612func (c *Context) RequireJobId() *Context {
613	if c.Err != nil {
614		return c
615	}
616
617	if !model.IsValidId(c.Params.JobId) {
618		c.SetInvalidURLParam("job_id")
619	}
620	return c
621}
622
623func (c *Context) RequireJobType() *Context {
624	if c.Err != nil {
625		return c
626	}
627
628	if c.Params.JobType == "" || len(c.Params.JobType) > 32 {
629		c.SetInvalidURLParam("job_type")
630	}
631	return c
632}
633
634func (c *Context) RequireRoleId() *Context {
635	if c.Err != nil {
636		return c
637	}
638
639	if !model.IsValidId(c.Params.RoleId) {
640		c.SetInvalidURLParam("role_id")
641	}
642	return c
643}
644
645func (c *Context) RequireSchemeId() *Context {
646	if c.Err != nil {
647		return c
648	}
649
650	if !model.IsValidId(c.Params.SchemeId) {
651		c.SetInvalidURLParam("scheme_id")
652	}
653	return c
654}
655
656func (c *Context) RequireRoleName() *Context {
657	if c.Err != nil {
658		return c
659	}
660
661	if !model.IsValidRoleName(c.Params.RoleName) {
662		c.SetInvalidURLParam("role_name")
663	}
664
665	return c
666}
667
668func (c *Context) RequireGroupId() *Context {
669	if c.Err != nil {
670		return c
671	}
672
673	if !model.IsValidId(c.Params.GroupId) {
674		c.SetInvalidURLParam("group_id")
675	}
676	return c
677}
678
679func (c *Context) RequireRemoteId() *Context {
680	if c.Err != nil {
681		return c
682	}
683
684	if c.Params.RemoteId == "" {
685		c.SetInvalidURLParam("remote_id")
686	}
687	return c
688}
689
690func (c *Context) RequireSyncableId() *Context {
691	if c.Err != nil {
692		return c
693	}
694
695	if !model.IsValidId(c.Params.SyncableId) {
696		c.SetInvalidURLParam("syncable_id")
697	}
698	return c
699}
700
701func (c *Context) RequireSyncableType() *Context {
702	if c.Err != nil {
703		return c
704	}
705
706	if c.Params.SyncableType != model.GroupSyncableTypeTeam && c.Params.SyncableType != model.GroupSyncableTypeChannel {
707		c.SetInvalidURLParam("syncable_type")
708	}
709	return c
710}
711
712func (c *Context) RequireBotUserId() *Context {
713	if c.Err != nil {
714		return c
715	}
716
717	if !model.IsValidId(c.Params.BotUserId) {
718		c.SetInvalidURLParam("bot_user_id")
719	}
720	return c
721}
722
723func (c *Context) RequireInvoiceId() *Context {
724	if c.Err != nil {
725		return c
726	}
727
728	if len(c.Params.InvoiceId) != 27 {
729		c.SetInvalidURLParam("invoice_id")
730	}
731
732	return c
733}
734
735func (c *Context) GetRemoteID(r *http.Request) string {
736	return r.Header.Get(model.HeaderRemoteclusterId)
737}
738