diff --git a/server/channels/api4/cloud.go b/server/channels/api4/cloud.go index 5e1eac99757..4cb19c50cac 100644 --- a/server/channels/api4/cloud.go +++ b/server/channels/api4/cloud.go @@ -65,8 +65,7 @@ func ensureCloudInterface(c *Context, where string) bool { return true } -func getPreviewSubscription(c *Context, w http.ResponseWriter, r *http.Request) { - license := c.App.Channels().License() +func getPreviewSubscription(c *Context, w http.ResponseWriter, r *http.Request, license *model.License) { subscription := &model.Subscription{ ID: "cloud-preview", ProductID: license.SkuName, @@ -90,8 +89,9 @@ func getPreviewSubscription(c *Context, w http.ResponseWriter, r *http.Request) func getSubscription(c *Context, w http.ResponseWriter, r *http.Request) { // Preview subscription is a special case for cloud preview licenses. - if c.App.Channels().License().IsCloudPreview() { - getPreviewSubscription(c, w, r) + license := c.App.Channels().License() + if license != nil && license.IsCloudPreview() { + getPreviewSubscription(c, w, r, license) return } @@ -100,7 +100,7 @@ func getSubscription(c *Context, w http.ResponseWriter, r *http.Request) { return } - if !c.App.Channels().License().IsCloud() { + if license == nil || !license.IsCloud() { c.Err = model.NewAppError("Api4.getSubscription", "api.cloud.license_error", nil, "", http.StatusForbidden) return } @@ -199,7 +199,8 @@ func validateWorkspaceBusinessEmail(c *Context, w http.ResponseWriter, r *http.R return } - if !c.App.Channels().License().IsCloud() { + license := c.App.Channels().License() + if license == nil || !license.IsCloud() { c.Err = model.NewAppError("Api4.validateWorkspaceBusinessEmail", "api.cloud.license_error", nil, "", http.StatusForbidden) return } @@ -250,7 +251,8 @@ func getCloudProducts(c *Context, w http.ResponseWriter, r *http.Request) { return } - if !c.App.Channels().License().IsCloud() { + license := c.App.Channels().License() + if license == nil || !license.IsCloud() { c.Err = model.NewAppError("Api4.getCloudProducts", "api.cloud.license_error", nil, "", http.StatusForbidden) return } @@ -300,7 +302,8 @@ func getCloudLimits(c *Context, w http.ResponseWriter, r *http.Request) { return } - if !c.App.Channels().License().IsCloud() { + license := c.App.Channels().License() + if license == nil || !license.IsCloud() { c.Err = model.NewAppError("Api4.getCloudLimits", "api.cloud.license_error", nil, "", http.StatusForbidden) return } @@ -328,7 +331,8 @@ func getCloudCustomer(c *Context, w http.ResponseWriter, r *http.Request) { return } - if !c.App.Channels().License().IsCloud() { + license := c.App.Channels().License() + if license == nil || !license.IsCloud() { c.Err = model.NewAppError("Api4.getCloudCustomer", "api.cloud.license_error", nil, "", http.StatusForbidden) return } @@ -384,7 +388,8 @@ func updateCloudCustomer(c *Context, w http.ResponseWriter, r *http.Request) { return } - if !c.App.Channels().License().IsCloud() { + license := c.App.Channels().License() + if license == nil || !license.IsCloud() { c.Err = model.NewAppError("Api4.updateCloudCustomer", "api.cloud.license_error", nil, "", http.StatusForbidden) return } @@ -429,7 +434,8 @@ func updateCloudCustomerAddress(c *Context, w http.ResponseWriter, r *http.Reque return } - if !c.App.Channels().License().IsCloud() { + license := c.App.Channels().License() + if license == nil || !license.IsCloud() { c.Err = model.NewAppError("Api4.updateCloudCustomerAddress", "api.cloud.license_error", nil, "", http.StatusForbidden) return } @@ -474,7 +480,8 @@ func getInvoicesForSubscription(c *Context, w http.ResponseWriter, r *http.Reque return } - if !c.App.Channels().License().IsCloud() { + license := c.App.Channels().License() + if license == nil || !license.IsCloud() { c.Err = model.NewAppError("Api4.getInvoicesForSubscription", "api.cloud.license_error", nil, "", http.StatusForbidden) return } @@ -507,7 +514,8 @@ func getSubscriptionInvoicePDF(c *Context, w http.ResponseWriter, r *http.Reques return } - if !c.App.Channels().License().IsCloud() { + license := c.App.Channels().License() + if license == nil || !license.IsCloud() { c.Err = model.NewAppError("Api4.getSubscriptionInvoicePDF", "api.cloud.license_error", nil, "", http.StatusForbidden) return } @@ -547,7 +555,8 @@ func handleCWSWebhook(c *Context, w http.ResponseWriter, r *http.Request) { return } - if !c.App.Channels().License().IsCloud() { + license := c.App.Channels().License() + if license == nil || !license.IsCloud() { c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.license_error", nil, "", http.StatusForbidden) return } diff --git a/server/channels/api4/cloud_test.go b/server/channels/api4/cloud_test.go index e3844bf01dd..67614ff68f1 100644 --- a/server/channels/api4/cloud_test.go +++ b/server/channels/api4/cloud_test.go @@ -486,3 +486,20 @@ func TestCheckCWSConnection(t *testing.T) { assert.Equal(t, "unavailable", response["status"]) }) } + +func TestGetSubscriptionNilLicense(t *testing.T) { + mainHelper.Parallel(t) + th := Setup(t) + + t.Run("nil license does not panic", func(t *testing.T) { + th.App.Srv().SetLicense(nil) + + // getSubscription calls License().IsCloudPreview() and License().IsCloud() + // — must not panic when license is nil. The exact error depends on the + // test environment (cloud interface may not be available), but the key + // assertion is no panic and no 500. + resp, err := th.SystemAdminClient.DoAPIGet(context.Background(), "/cloud/subscription", "") + require.Error(t, err) + require.NotEqual(t, http.StatusInternalServerError, resp.StatusCode) + }) +} diff --git a/server/channels/api4/config.go b/server/channels/api4/config.go index c35ec4daa86..4af473f575e 100644 --- a/server/channels/api4/config.go +++ b/server/channels/api4/config.go @@ -80,7 +80,8 @@ func getConfig(c *Context, w http.ResponseWriter, r *http.Request) { RemoveMasked: filterMasked, }, } - if c.App.Channels().License().IsCloud() { + license := c.App.Channels().License() + if license != nil && license.IsCloud() { filterOpts.TagFilters = append(filterOpts.TagFilters, model.FilterTag{ TagType: model.ConfigAccessTagType, TagName: model.ConfigAccessTagCloudRestrictable, @@ -168,7 +169,8 @@ func updateConfig(c *Context, w http.ResponseWriter, r *http.Request) { } // There are some settings that cannot be changed in a cloud env - if c.App.Channels().License().IsCloud() { + license := c.App.Channels().License() + if license != nil && license.IsCloud() { // Both of them cannot be nil since cfg.SetDefaults is called earlier for cfg, // and appCfg is the existing earlier config and if it's nil, server sets a default value. if *appCfg.ComplianceSettings.Directory != *cfg.ComplianceSettings.Directory { @@ -233,7 +235,8 @@ func updateConfig(c *Context, w http.ResponseWriter, r *http.Request) { c.LogAudit("updateConfig") w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") - if c.App.Channels().License().IsCloud() { + license = c.App.Channels().License() + if license != nil && license.IsCloud() { js, err := cfg.ToJSONFiltered(model.ConfigAccessTagType, model.ConfigAccessTagCloudRestrictable) if err != nil { c.Err = model.NewAppError("updateConfig", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err) @@ -324,7 +327,8 @@ func patchConfig(c *Context, w http.ResponseWriter, r *http.Request) { } // There are some settings that cannot be changed in a cloud env - if c.App.Channels().License().IsCloud() { + license := c.App.Channels().License() + if license != nil && license.IsCloud() { if cfg.ComplianceSettings.Directory != nil && *appCfg.ComplianceSettings.Directory != *cfg.ComplianceSettings.Directory { c.Err = model.NewAppError("patchConfig", "api.config.update_config.not_allowed_security.app_error", map[string]any{"Name": "ComplianceSettings.Directory"}, "", http.StatusForbidden) return @@ -383,7 +387,8 @@ func patchConfig(c *Context, w http.ResponseWriter, r *http.Request) { } w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") - if c.App.Channels().License().IsCloud() { + license = c.App.Channels().License() + if license != nil && license.IsCloud() { js, err := cfg.ToJSONFiltered(model.ConfigAccessTagType, model.ConfigAccessTagCloudRestrictable) if err != nil { c.Err = model.NewAppError("patchConfig", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err) diff --git a/server/channels/api4/config_test.go b/server/channels/api4/config_test.go index 6c50bdf0127..20afdd2b428 100644 --- a/server/channels/api4/config_test.go +++ b/server/channels/api4/config_test.go @@ -66,6 +66,14 @@ func TestGetConfig(t *testing.T) { require.NotEqual(t, model.FakeSetting, *cfg.SqlSettings.DataSource) require.NotEqual(t, model.FakeSetting, *cfg.FileSettings.PublicLinkSalt) }) + + t.Run("nil license does not panic", func(t *testing.T) { + th.App.Srv().SetLicense(nil) + + // GetConfig calls License().IsCloud() — must not panic when license is nil + _, _, err := th.SystemAdminClient.GetConfig(context.Background()) + require.NoError(t, err) + }) } func TestGetConfigWithAccessTag(t *testing.T) { diff --git a/server/channels/api4/license.go b/server/channels/api4/license.go index f9dc659a5ac..5d24a592ead 100644 --- a/server/channels/api4/license.go +++ b/server/channels/api4/license.go @@ -124,7 +124,7 @@ func addLicense(c *Context, w http.ResponseWriter, r *http.Request) { } } - license, appErr = c.App.Srv().SaveLicense(licenseBytes) + _, appErr = c.App.Srv().SaveLicense(licenseBytes) if appErr != nil { if appErr.Id == model.ExpiredLicenseError { c.LogAudit("failed - expired or non-started license") @@ -137,7 +137,8 @@ func addLicense(c *Context, w http.ResponseWriter, r *http.Request) { return } - if c.App.Channels().License().IsCloud() { + license = c.App.Channels().License() + if license != nil && license.IsCloud() { // If cloud, invalidate the caches when a new license is loaded defer func() { if err := c.App.Srv().Cloud.HandleLicenseChange(); err != nil { diff --git a/server/channels/api4/license_test.go b/server/channels/api4/license_test.go index dfed4bd3b00..a27a0ac1849 100644 --- a/server/channels/api4/license_test.go +++ b/server/channels/api4/license_test.go @@ -618,3 +618,18 @@ func TestGetLicenseLoadMetric(t *testing.T) { require.Equal(t, 1500, loadValue) }) } + +func TestAddLicenseNilLicense(t *testing.T) { + mainHelper.Parallel(t) + th := Setup(t).InitBasic(t) + + t.Run("nil license does not panic", func(t *testing.T) { + th.App.Srv().SetLicense(nil) + + // addLicense checks License().IsCloud() after saving — but the upload + // itself will fail validation. Key assertion: no panic, no 500. + resp, err := th.SystemAdminClient.DoAPIPost(context.Background(), "/license", "not-a-real-license") + require.Error(t, err) + require.NotEqual(t, http.StatusInternalServerError, resp.StatusCode) + }) +} diff --git a/server/channels/api4/team.go b/server/channels/api4/team.go index 4b2c0f37ab8..e5cb328411f 100644 --- a/server/channels/api4/team.go +++ b/server/channels/api4/team.go @@ -99,7 +99,8 @@ func createTeam(c *Context, w http.ResponseWriter, r *http.Request) { } // On a cloud license, we must check limits before allowing to create - if c.App.Channels().License().IsCloud() { + license = c.App.Channels().License() + if license != nil && license.IsCloud() { limits, err := c.App.Cloud().GetCloudLimits(c.AppContext.Session().UserId) if err != nil { c.Err = model.NewAppError("Api4.createTeam", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err) @@ -405,7 +406,8 @@ func restoreTeam(c *Context, w http.ResponseWriter, r *http.Request) { return } // On a cloud license, we must check limits before allowing to restore - if c.App.Channels().License().IsCloud() { + license := c.App.Channels().License() + if license != nil && license.IsCloud() { limits, err := c.App.Cloud().GetCloudLimits(c.AppContext.Session().UserId) if err != nil { c.Err = model.NewAppError("Api4.restoreTeam", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err) @@ -1462,7 +1464,8 @@ func teamExists(c *Context, w http.ResponseWriter, r *http.Request) { } func importTeam(c *Context, w http.ResponseWriter, r *http.Request) { - if c.App.Channels().License().IsCloud() { + license := c.App.Channels().License() + if license != nil && license.IsCloud() { c.Err = model.NewAppError("importTeam", "api.restricted_system_admin", nil, "", http.StatusForbidden) return } @@ -1701,7 +1704,8 @@ func inviteGuestsToChannels(c *Context, w http.ResponseWriter, r *http.Request) return } - guestEnabled := c.App.Channels().License() != nil && *c.App.Channels().License().Features.GuestAccounts + license := c.App.Channels().License() + guestEnabled := license != nil && license.Features != nil && license.Features.GuestAccounts != nil && *license.Features.GuestAccounts if !guestEnabled { c.Err = model.NewAppError("Api4.InviteGuestsToChannels", "api.team.invite_guests_to_channels.disabled.error", nil, "", http.StatusForbidden) diff --git a/server/channels/api4/team_test.go b/server/channels/api4/team_test.go index 8f480dbd2a5..53e50e6f362 100644 --- a/server/channels/api4/team_test.go +++ b/server/channels/api4/team_test.go @@ -4,11 +4,13 @@ package api4 import ( + "bytes" "context" "encoding/base64" "encoding/binary" "encoding/json" "fmt" + "mime/multipart" "net/http" "strconv" "strings" @@ -235,6 +237,17 @@ func TestCreateTeam(t *testing.T) { require.NoError(t, err) CheckCreatedStatus(t, resp) }) + + t.Run("nil license does not panic", func(t *testing.T) { + th.App.Srv().SetLicense(nil) + + team := &model.Team{Name: GenerateTestUsername(), DisplayName: "No License Team", Type: model.TeamOpen} + _, resp, err := th.Client.CreateTeam(context.Background(), team) + // The request may succeed or fail depending on permissions, + // but it must NOT panic due to nil License().IsCloud() + require.NoError(t, err) + require.NotEqual(t, http.StatusInternalServerError, resp.StatusCode) + }) } func TestCreateTeamSanitization(t *testing.T) { @@ -5062,3 +5075,49 @@ func TestGetTeamMembersForUserRoleDataSanitization(t *testing.T) { require.Fail(t, "basic team membership not found") }) } + +func TestRestoreTeamNilLicense(t *testing.T) { + mainHelper.Parallel(t) + th := Setup(t).InitBasic(t) + + t.Run("nil license does not panic", func(t *testing.T) { + th.App.Srv().SetLicense(nil) + + // restoreTeam checks License().IsCloud() — must not panic when nil. + resp, err := th.SystemAdminClient.DoAPIPost(context.Background(), "/teams/"+th.BasicTeam.Id+"/restore", "") + // May return 403/404 depending on team state, but must not return 500. + if err != nil { + require.NotEqual(t, http.StatusInternalServerError, resp.StatusCode) + } + }) +} + +func TestImportTeamNilLicense(t *testing.T) { + mainHelper.Parallel(t) + th := Setup(t).InitBasic(t) + + t.Run("nil license does not panic", func(t *testing.T) { + th.App.Srv().SetLicense(nil) + + // importTeam checks License().IsCloud() — must not panic when nil. + // We send a minimal multipart request that will fail validation but should not panic. + var b bytes.Buffer + w := multipart.NewWriter(&b) + require.NoError(t, w.WriteField("importFrom", "slack")) + require.NoError(t, w.WriteField("filesize", "100")) + part, err := w.CreateFormFile("file", "import.zip") + require.NoError(t, err) + _, err = part.Write([]byte("fake")) + require.NoError(t, err) + w.Close() + + req, _ := http.NewRequest("POST", th.SystemAdminClient.APIURL+"/teams/"+th.BasicTeam.Id+"/import", &b) + req.Header.Set("Content-Type", w.FormDataContentType()) + req.Header.Set(model.HeaderAuth, model.HeaderBearer+" "+th.SystemAdminClient.AuthToken) + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + resp.Body.Close() + require.NotEqual(t, http.StatusInternalServerError, resp.StatusCode, + "nil license must not cause a 500 panic in importTeam") + }) +} diff --git a/server/channels/api4/upload.go b/server/channels/api4/upload.go index f169b8d5f00..2a979c181ac 100644 --- a/server/channels/api4/upload.go +++ b/server/channels/api4/upload.go @@ -52,7 +52,8 @@ func createUpload(c *Context, w http.ResponseWriter, r *http.Request) { c.SetPermissionError(model.PermissionManageSystem) return } - if c.App.Srv().License().IsCloud() { + license := c.App.Srv().License() + if license != nil && license.IsCloud() { c.Err = model.NewAppError("createUpload", "api.file.cloud_upload.app_error", nil, "", http.StatusBadRequest) return } @@ -147,7 +148,8 @@ func uploadData(c *Context, w http.ResponseWriter, r *http.Request) { c.SetPermissionError(model.PermissionManageSystem) return } - if c.App.Srv().License().IsCloud() { + license := c.App.Srv().License() + if license != nil && license.IsCloud() { c.Err = model.NewAppError("UploadData", "api.file.cloud_upload.app_error", nil, "", http.StatusBadRequest) return } diff --git a/server/channels/api4/upload_test.go b/server/channels/api4/upload_test.go index 2c99f7e47c0..b5b5fac2d3e 100644 --- a/server/channels/api4/upload_test.go +++ b/server/channels/api4/upload_test.go @@ -551,3 +551,22 @@ func TestUploadDataMultipart(t *testing.T) { require.Equal(t, file, data) }) } + +func TestCreateUploadNilLicense(t *testing.T) { + mainHelper.Parallel(t) + th := Setup(t).InitBasic(t) + + t.Run("nil license does not panic on import upload", func(t *testing.T) { + th.App.Srv().SetLicense(nil) + + us := &model.UploadSession{ + Type: model.UploadTypeImport, + Filename: "import.zip", + FileSize: 1024, + } + _, resp, err := th.SystemAdminClient.CreateUpload(context.Background(), us) + require.Error(t, err) + require.NotEqual(t, http.StatusInternalServerError, resp.StatusCode, + "nil license must not cause a 500 panic in createUpload") + }) +} diff --git a/server/channels/api4/user.go b/server/channels/api4/user.go index 545de311912..8b52599bace 100644 --- a/server/channels/api4/user.go +++ b/server/channels/api4/user.go @@ -2276,7 +2276,8 @@ func loginCWS(c *Context, w http.ResponseWriter, r *http.Request) { "cyber-defense": "/cyber-defense-hq", } - if !c.App.Channels().License().IsCloud() { + license := c.App.Channels().License() + if license == nil || !license.IsCloud() { c.Err = model.NewAppError("loginCWS", "api.user.login_cws.license.error", nil, "", http.StatusUnauthorized) return } @@ -2335,7 +2336,8 @@ func loginCWS(c *Context, w http.ResponseWriter, r *http.Request) { } // If a cloud preview, redirect to the correct use case URL - if c.App.License().IsCloudPreview() && useCase != "" { + license = c.App.License() + if license != nil && license.IsCloudPreview() && useCase != "" { if url, ok := useCaseToURL[useCase]; ok { redirectURL += url } @@ -3282,7 +3284,8 @@ func demoteUserToGuest(c *Context, w http.ResponseWriter, r *http.Request) { return } - guestEnabled := c.App.Channels().License() != nil && *c.App.Channels().License().Features.GuestAccounts + license := c.App.Channels().License() + guestEnabled := license != nil && license.Features != nil && license.Features.GuestAccounts != nil && *license.Features.GuestAccounts if !guestEnabled { c.Err = model.NewAppError("Api4.demoteUserToGuest", "api.team.invite_guests_to_channels.disabled.error", nil, "", http.StatusForbidden) @@ -3577,7 +3580,8 @@ func migrateAuthToLDAP(c *Context, w http.ResponseWriter, r *http.Request) { return } - if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAP { + license := c.App.Channels().License() + if license == nil || license.Features == nil || license.Features.LDAP == nil || !*license.Features.LDAP { c.Err = model.NewAppError("api.migrateAuthToLDAP", "api.admin.ldap.not_available.app_error", nil, "", http.StatusNotImplemented) return } @@ -3636,7 +3640,8 @@ func migrateAuthToSaml(c *Context, w http.ResponseWriter, r *http.Request) { return } - if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.SAML { + license := c.App.Channels().License() + if license == nil || license.Features == nil || license.Features.SAML == nil || !*license.Features.SAML { c.Err = model.NewAppError("api.migrateAuthToSaml", "api.admin.saml.not_available.app_error", nil, "", http.StatusNotImplemented) return } diff --git a/server/channels/api4/user_test.go b/server/channels/api4/user_test.go index c0024292995..c0013eba712 100644 --- a/server/channels/api4/user_test.go +++ b/server/channels/api4/user_test.go @@ -10268,3 +10268,61 @@ func TestSearchUsersWithMfaEnforced(t *testing.T) { CheckForbiddenStatus(t, resp) }) } + +func TestLoginNilLicense(t *testing.T) { + mainHelper.Parallel(t) + th := Setup(t).InitBasic(t) + + t.Run("nil license does not panic on login", func(t *testing.T) { + th.App.Srv().SetLicense(nil) + _, err := th.Client.Logout(context.Background()) + require.NoError(t, err) + + // Login calls isCWSLogin and AttachSessionCookies, both of which + // reference License().IsCloud() — must not panic when license is nil. + _, _, err = th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password) + require.NoError(t, err) + }) +} + +func TestDemoteUserToGuestNilLicense(t *testing.T) { + mainHelper.Parallel(t) + th := Setup(t).InitBasic(t) + + t.Run("nil license does not panic", func(t *testing.T) { + th.App.Srv().SetLicense(nil) + + // demoteUserToGuest checks License() == nil — should return 501, not panic. + resp, err := th.SystemAdminClient.DoAPIPost(context.Background(), "/users/"+th.BasicUser.Id+"/demote", "") + require.Error(t, err) + require.NotEqual(t, http.StatusInternalServerError, resp.StatusCode) + }) +} + +func TestMigrateAuthToLDAPNilLicense(t *testing.T) { + mainHelper.Parallel(t) + th := Setup(t).InitBasic(t) + + t.Run("nil license does not panic", func(t *testing.T) { + th.App.Srv().SetLicense(nil) + + jsonBody := `{"from":"email","force":false,"match_field":"email"}` + resp, err := th.SystemAdminClient.DoAPIPost(context.Background(), "/users/migrate_auth/ldap", jsonBody) + require.Error(t, err) + require.NotEqual(t, http.StatusInternalServerError, resp.StatusCode) + }) +} + +func TestMigrateAuthToSamlNilLicense(t *testing.T) { + mainHelper.Parallel(t) + th := Setup(t).InitBasic(t) + + t.Run("nil license does not panic", func(t *testing.T) { + th.App.Srv().SetLicense(nil) + + jsonBody := `{"from":"email","auto":false,"matches":{}}` + resp, err := th.SystemAdminClient.DoAPIPost(context.Background(), "/users/migrate_auth/saml", jsonBody) + require.Error(t, err) + require.NotEqual(t, http.StatusInternalServerError, resp.StatusCode) + }) +} diff --git a/server/channels/app/audit.go b/server/channels/app/audit.go index 2b8a6f9e732..8e84596b598 100644 --- a/server/channels/app/audit.go +++ b/server/channels/app/audit.go @@ -198,7 +198,8 @@ func (a *App) AddAuditLogCertificate(rctx request.CTX, fileData *multipart.FileH a.UpdateConfig(func(dest *model.Config) { *dest = *cfg }) - if a.License().IsCloud() { + license := a.License() + if license != nil && license.IsCloud() { err = a.Cloud().CreateAuditLoggingCert(rctx.Session().UserId, fileData) if err != nil { return model.NewAppError("AddAuditLogCertificate", "api.admin.add_certificate.app_error", nil, "", http.StatusInternalServerError).Wrap(err) @@ -224,7 +225,8 @@ func (a *App) RemoveAuditLogCertificate(rctx request.CTX) *model.AppError { a.UpdateConfig(func(dest *model.Config) { *dest = *cfg }) - if a.License().IsCloud() { + license := a.License() + if license != nil && license.IsCloud() { err = a.Cloud().RemoveAuditLoggingCert(rctx.Session().UserId) if err != nil { return model.NewAppError("RemoveAuditLogCertificate", "api.admin.remove_certificate.app_error", nil, "", http.StatusInternalServerError).Wrap(err) diff --git a/server/channels/app/file.go b/server/channels/app/file.go index 2188b2488b3..d35bc423d17 100644 --- a/server/channels/app/file.go +++ b/server/channels/app/file.go @@ -61,7 +61,8 @@ func (a *App) ExportFileBackend() filestore.FileBackend { func (a *App) CheckMandatoryS3Fields(settings *model.FileSettings) *model.AppError { var fileBackendSettings filestore.FileBackendSettings - if a.License().IsCloud() && a.Config().FeatureFlags.CloudDedicatedExportUI && a.Config().FileSettings.DedicatedExportStore != nil && *a.Config().FileSettings.DedicatedExportStore { + license := a.License() + if license != nil && license.IsCloud() && a.Config().FeatureFlags.CloudDedicatedExportUI && a.Config().FileSettings.DedicatedExportStore != nil && *a.Config().FileSettings.DedicatedExportStore { fileBackendSettings = filestore.NewExportFileBackendSettingsFromConfig(settings, false, false) } else { fileBackendSettings = filestore.NewFileBackendSettingsFromConfig(settings, false, false) diff --git a/server/channels/app/file_test.go b/server/channels/app/file_test.go index d30bda3e40c..a3bc4369626 100644 --- a/server/channels/app/file_test.go +++ b/server/channels/app/file_test.go @@ -1207,3 +1207,18 @@ func TestFilterFilesByChannelPermissions_ABAC(t *testing.T) { mockACS.AssertNotCalled(t, "AccessEvaluation") }) } + +func TestCheckMandatoryS3FieldsNilLicense(t *testing.T) { + mainHelper.Parallel(t) + th := Setup(t) + + t.Run("nil license does not panic", func(t *testing.T) { + th.App.Srv().SetLicense(nil) + + settings := &model.FileSettings{} + settings.SetDefaults(false) + // CheckMandatoryS3Fields calls License().IsCloud() — must not panic when nil. + _ = th.App.CheckMandatoryS3Fields(settings) + // If we get here without a panic, the test passes. + }) +} diff --git a/server/channels/app/ip_filtering.go b/server/channels/app/ip_filtering.go index 2afb9ef4bee..54d2d3b5c69 100644 --- a/server/channels/app/ip_filtering.go +++ b/server/channels/app/ip_filtering.go @@ -10,7 +10,8 @@ import ( func (a *App) SendIPFiltersChangedEmail(rctx request.CTX, userID string) error { cloudWorkspaceOwnerEmailAddress := "" - if a.License().IsCloud() { + license := a.License() + if license != nil && license.IsCloud() { portalUserCustomer, cErr := a.Cloud().GetCloudCustomer(userID) if cErr != nil { rctx.Logger().Error("Failed to get portal user customer", mlog.Err(cErr)) diff --git a/server/channels/app/ip_filtering_test.go b/server/channels/app/ip_filtering_test.go new file mode 100644 index 00000000000..34d30b47395 --- /dev/null +++ b/server/channels/app/ip_filtering_test.go @@ -0,0 +1,25 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package app + +import ( + "testing" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/request" +) + +func TestSendIPFiltersChangedEmailNilLicense(t *testing.T) { + mainHelper.Parallel(t) + th := Setup(t) + + t.Run("nil license does not panic", func(t *testing.T) { + th.App.Srv().SetLicense(nil) + + rctx := request.TestContext(t) + // SendIPFiltersChangedEmail checks License().IsCloud() — must not panic when nil. + // It may return an error (e.g., no SMTP configured), but must not panic. + _ = th.App.SendIPFiltersChangedEmail(rctx, model.NewId()) + }) +} diff --git a/server/channels/app/login.go b/server/channels/app/login.go index 8f20a8c2f41..190a115cb28 100644 --- a/server/channels/app/login.go +++ b/server/channels/app/login.go @@ -197,7 +197,8 @@ func (a *App) DoLogin(rctx request.CTX, w http.ResponseWriter, r *http.Request, rctx = rctx.WithSession(session) - if a.Srv().License() != nil && *a.Srv().License().Features.LDAP && a.Ldap() != nil { + license := a.Srv().License() + if license != nil && *license.Features.LDAP && a.Ldap() != nil { userVal := *user sessionVal := *session a.Srv().Go(func() { @@ -313,7 +314,8 @@ func (a *App) AttachSessionCookies(rctx request.CTX, w http.ResponseWriter, r *h http.SetCookie(w, csrfCookie) // For context see: https://mattermost.atlassian.net/browse/MM-39583 - if a.License().IsCloud() { + license := a.License() + if license != nil && license.IsCloud() { a.AttachCloudSessionCookie(rctx, w, r) } } @@ -326,5 +328,6 @@ func GetProtocol(r *http.Request) string { } func isCWSLogin(a *App, token string) bool { - return a.License().IsCloud() && token != "" + license := a.License() + return license != nil && license.IsCloud() && token != "" } diff --git a/server/channels/app/server.go b/server/channels/app/server.go index 4bdd3e5e173..2b48b6f8ffb 100644 --- a/server/channels/app/server.go +++ b/server/channels/app/server.go @@ -1445,7 +1445,8 @@ func (s *Server) sendLicenseUpForRenewalEmail(users map[string]*model.User, lice func (s *Server) doReportUserCountForCloudSubscriptionJob() { s.LoadLicense() - if !s.License().IsCloud() { + license := s.License() + if license == nil || !license.IsCloud() { return } diff --git a/server/channels/web/handlers.go b/server/channels/web/handlers.go index 8a7f1723594..1c0eb8a0afa 100644 --- a/server/channels/web/handlers.go +++ b/server/channels/web/handlers.go @@ -217,7 +217,8 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { subpath, _ := utils.GetSubpathFromConfig(c.App.Config()) siteURLHeader := app.GetProtocol(r) + "://" + r.Host + subpath - if c.App.Channels().License().IsCloud() { + license := c.App.Channels().License() + if license != nil && license.IsCloud() { siteURLHeader = *c.App.Config().ServiceSettings.SiteURL + subpath } c.SetSiteURLHeader(siteURLHeader) @@ -289,7 +290,7 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { c.RemoveSessionCookie(w, r) c.Err = model.NewAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token+" Appears to be a CSRF attempt", http.StatusUnauthorized) } - } else if token != "" && c.App.Channels().License().IsCloud() && tokenLocation == app.TokenLocationCloudHeader { + } else if token != "" && license != nil && license.IsCloud() && tokenLocation == app.TokenLocationCloudHeader { // Check to see if this provided token matches our CWS Token session, err := c.App.GetCloudSession(token) if err != nil { @@ -298,7 +299,7 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } else { c.AppContext = c.AppContext.WithSession(session) } - } else if token != "" && c.App.Channels().License() != nil && c.App.Channels().License().HasRemoteClusterService() && tokenLocation == app.TokenLocationRemoteClusterHeader { + } else if token != "" && license != nil && license.HasRemoteClusterService() && tokenLocation == app.TokenLocationRemoteClusterHeader { // Get the remote cluster if remoteId := c.GetRemoteID(r); remoteId == "" { c.Logger.Warn("Missing remote cluster id") // diff --git a/server/channels/web/handlers_test.go b/server/channels/web/handlers_test.go index a3b5f0c67c8..c2c7ac26fec 100644 --- a/server/channels/web/handlers_test.go +++ b/server/channels/web/handlers_test.go @@ -1223,3 +1223,21 @@ func TestHandleContextErrorZeroStatusCode(t *testing.T) { assert.Equal(t, http.StatusBadRequest, response.Code) }) } + +func TestHandlerServeHTTPNilLicense(t *testing.T) { + th := Setup(t) + + th.App.Srv().SetLicense(nil) + + web := New(th.Server) + handler := web.NewHandler(handlerForServeDefaultSecurityHeaders) + + // ServeHTTP checks License().IsCloud() for siteURL and CWS token — must not panic when nil. + request := httptest.NewRequest("GET", "/api/v4/test", nil) + response := httptest.NewRecorder() + handler.ServeHTTP(response, request) + + // Should complete without panic. Any non-500 status is acceptable. + require.NotEqual(t, http.StatusInternalServerError, response.Code, + "nil license must not cause a 500 panic in ServeHTTP") +}