mirror of
https://github.com/mattermost/mattermost.git
synced 2026-05-28 04:35:04 -04:00
[MM-68649] Add Session Attributes from user agent for use in Permission Policies (#36511)
Some checks are pending
API / build (push) Waiting to run
Server CI / Compute Go Version (push) Waiting to run
Server CI / Check mocks (push) Blocked by required conditions
Server CI / Check go mod tidy (push) Blocked by required conditions
Server CI / Check go fix (push) Blocked by required conditions
Server CI / check-style (push) Blocked by required conditions
Server CI / Check serialization methods for hot structs (push) Blocked by required conditions
Server CI / Vet API (push) Blocked by required conditions
Server CI / Check migration files (push) Blocked by required conditions
Server CI / Generate email templates (push) Blocked by required conditions
Server CI / Check store layers (push) Blocked by required conditions
Server CI / Check default roles permissions (push) Blocked by required conditions
Server CI / Check mmctl docs (push) Blocked by required conditions
Server CI / Postgres (shard 0) (push) Blocked by required conditions
Server CI / Postgres (shard 1) (push) Blocked by required conditions
Server CI / Postgres (shard 2) (push) Blocked by required conditions
Server CI / Postgres (shard 3) (push) Blocked by required conditions
Server CI / Merge Postgres Test Results (push) Blocked by required conditions
Server CI / Elasticsearch v8 Compatibility (push) Blocked by required conditions
Server CI / OpenSearch v2 Compatibility (push) Blocked by required conditions
Server CI / Postgres FIPS (shard 0) (push) Blocked by required conditions
Server CI / Postgres FIPS (shard 1) (push) Blocked by required conditions
Server CI / Postgres FIPS (shard 2) (push) Blocked by required conditions
Server CI / Postgres FIPS (shard 3) (push) Blocked by required conditions
Server CI / Merge Postgres FIPS Test Results (push) Blocked by required conditions
Server CI / Run mmctl tests (push) Blocked by required conditions
Server CI / Run mmctl tests (FIPS) (push) Blocked by required conditions
Server CI / Build mattermost server app (push) Blocked by required conditions
Tools CI / check-style (mattermost-govet) (push) Waiting to run
Tools CI / Test (mattermost-govet) (push) Waiting to run
Web App CI / check-lint (push) Waiting to run
Web App CI / check-i18n (push) Blocked by required conditions
Web App CI / check-external-links (push) Blocked by required conditions
Web App CI / check-types (push) Blocked by required conditions
Web App CI / test (platform) (push) Blocked by required conditions
Web App CI / test (mattermost-redux) (push) Blocked by required conditions
Web App CI / test (channels shard 1/4) (push) Blocked by required conditions
Web App CI / test (channels shard 2/4) (push) Blocked by required conditions
Web App CI / test (channels shard 3/4) (push) Blocked by required conditions
Web App CI / test (channels shard 4/4) (push) Blocked by required conditions
Web App CI / upload-coverage (push) Blocked by required conditions
Web App CI / build (push) Blocked by required conditions
YAML Lint / yamllint (push) Waiting to run
Some checks are pending
API / build (push) Waiting to run
Server CI / Compute Go Version (push) Waiting to run
Server CI / Check mocks (push) Blocked by required conditions
Server CI / Check go mod tidy (push) Blocked by required conditions
Server CI / Check go fix (push) Blocked by required conditions
Server CI / check-style (push) Blocked by required conditions
Server CI / Check serialization methods for hot structs (push) Blocked by required conditions
Server CI / Vet API (push) Blocked by required conditions
Server CI / Check migration files (push) Blocked by required conditions
Server CI / Generate email templates (push) Blocked by required conditions
Server CI / Check store layers (push) Blocked by required conditions
Server CI / Check default roles permissions (push) Blocked by required conditions
Server CI / Check mmctl docs (push) Blocked by required conditions
Server CI / Postgres (shard 0) (push) Blocked by required conditions
Server CI / Postgres (shard 1) (push) Blocked by required conditions
Server CI / Postgres (shard 2) (push) Blocked by required conditions
Server CI / Postgres (shard 3) (push) Blocked by required conditions
Server CI / Merge Postgres Test Results (push) Blocked by required conditions
Server CI / Elasticsearch v8 Compatibility (push) Blocked by required conditions
Server CI / OpenSearch v2 Compatibility (push) Blocked by required conditions
Server CI / Postgres FIPS (shard 0) (push) Blocked by required conditions
Server CI / Postgres FIPS (shard 1) (push) Blocked by required conditions
Server CI / Postgres FIPS (shard 2) (push) Blocked by required conditions
Server CI / Postgres FIPS (shard 3) (push) Blocked by required conditions
Server CI / Merge Postgres FIPS Test Results (push) Blocked by required conditions
Server CI / Run mmctl tests (push) Blocked by required conditions
Server CI / Run mmctl tests (FIPS) (push) Blocked by required conditions
Server CI / Build mattermost server app (push) Blocked by required conditions
Tools CI / check-style (mattermost-govet) (push) Waiting to run
Tools CI / Test (mattermost-govet) (push) Waiting to run
Web App CI / check-lint (push) Waiting to run
Web App CI / check-i18n (push) Blocked by required conditions
Web App CI / check-external-links (push) Blocked by required conditions
Web App CI / check-types (push) Blocked by required conditions
Web App CI / test (platform) (push) Blocked by required conditions
Web App CI / test (mattermost-redux) (push) Blocked by required conditions
Web App CI / test (channels shard 1/4) (push) Blocked by required conditions
Web App CI / test (channels shard 2/4) (push) Blocked by required conditions
Web App CI / test (channels shard 3/4) (push) Blocked by required conditions
Web App CI / test (channels shard 4/4) (push) Blocked by required conditions
Web App CI / upload-coverage (push) Blocked by required conditions
Web App CI / build (push) Blocked by required conditions
YAML Lint / yamllint (push) Waiting to run
* [MM-68649] Add Session Attributes from user agent for use in Permission Policies * Update server/channels/app/session_attributes.go Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Fix test * fix i18n * Allow session attributes for permission policies when no user attributes are configured --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Mattermost Build <build@mattermost.com> Co-authored-by: maria.nunez <maria.nunez@mattermost.com>
This commit is contained in:
parent
16c8f9d6e3
commit
d9c1388461
31 changed files with 907 additions and 75 deletions
|
|
@ -16,6 +16,7 @@ import (
|
|||
"github.com/mattermost/mattermost/server/public/shared/request"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/store"
|
||||
"github.com/mattermost/mattermost/server/v8/einterfaces"
|
||||
"github.com/mattermost/mattermost/server/v8/platform/services/cache"
|
||||
)
|
||||
|
||||
const attributeViewRefreshInterval = 30 * time.Second
|
||||
|
|
@ -2071,6 +2072,7 @@ func (a *App) BuildAccessControlSubject(rctx request.CTX, userID string, roles s
|
|||
ID: userID,
|
||||
Type: "user",
|
||||
Attributes: map[string]any{},
|
||||
Session: map[string]any{},
|
||||
}
|
||||
} else {
|
||||
rctx.Logger().Warn("Failed to get subject for access control subject",
|
||||
|
|
@ -2108,6 +2110,23 @@ func (a *App) BuildAccessControlSubject(rctx request.CTX, userID string, roles s
|
|||
return subject, nil
|
||||
}
|
||||
|
||||
func (a *App) BuildAccessControlSubjectForSession(rctx request.CTX, channelID string) (*model.Subject, *model.AppError) {
|
||||
subject, appErr := a.BuildAccessControlSubject(rctx, rctx.Session().UserId, rctx.Session().Roles, channelID)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
attrs, err := a.Srv().Store().SessionAttribute().Get(rctx.Session().Id)
|
||||
if err != nil {
|
||||
if errors.Is(err, cache.ErrKeyNotFound) {
|
||||
return subject, nil
|
||||
}
|
||||
return nil, model.NewAppError("BuildAccessControlSubjectForSession", "app.access_control.build_subject_for_session.get_session_attributes.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
subject.Session = attrs
|
||||
return subject, nil
|
||||
}
|
||||
|
||||
// GetSubjectChannelRole returns the channel-scoped role identifier
|
||||
// (channel_admin / channel_user / channel_guest) for the given user in
|
||||
// the given channel.
|
||||
|
|
|
|||
|
|
@ -2319,6 +2319,41 @@ func TestGetRecommendedPublicChannelsForUser(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestBuildAccessControlSubjectForSession(t *testing.T) {
|
||||
t.Run("returns subject without session attributes when none are cached", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
session, appErr := th.App.CreateSession(th.Context, &model.Session{UserId: th.BasicUser.Id, Props: model.StringMap{}})
|
||||
require.Nil(t, appErr)
|
||||
rctx := th.Context.WithSession(session)
|
||||
|
||||
subject, appErr := th.App.BuildAccessControlSubjectForSession(rctx, "")
|
||||
require.Nil(t, appErr)
|
||||
require.NotNil(t, subject)
|
||||
assert.Equal(t, th.BasicUser.Id, subject.ID)
|
||||
assert.Empty(t, subject.Session)
|
||||
})
|
||||
|
||||
t.Run("populates session attributes from the cache", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
session, appErr := th.App.CreateSession(th.Context, &model.Session{UserId: th.BasicUser.Id, Props: model.StringMap{}})
|
||||
require.Nil(t, appErr)
|
||||
rctx := th.Context.WithSession(session)
|
||||
|
||||
require.NoError(t, th.App.Srv().Store().SessionAttribute().Refresh(session.Id, map[string]any{
|
||||
model.SessionAttributesPropertyFieldIPAddress: "192.0.2.10",
|
||||
model.SessionAttributesPropertyFieldUserAgentBrowserName: "Chrome",
|
||||
}))
|
||||
|
||||
subject, appErr := th.App.BuildAccessControlSubjectForSession(rctx, "")
|
||||
require.Nil(t, appErr)
|
||||
require.NotNil(t, subject)
|
||||
assert.Equal(t, "192.0.2.10", subject.Session[model.SessionAttributesPropertyFieldIPAddress])
|
||||
assert.Equal(t, "Chrome", subject.Session[model.SessionAttributesPropertyFieldUserAgentBrowserName])
|
||||
})
|
||||
}
|
||||
|
||||
// TestFilterResponseToEditingRuleScope locks down the post-processing
|
||||
// that turns a full-stack simulator response into a "this rule only"
|
||||
// view. Upper-scoped blame entries (system_permission, peer_policy,
|
||||
|
|
|
|||
|
|
@ -730,7 +730,13 @@ func (a *App) HasPermissionToFileAction(rctx request.CTX, userID string, roles s
|
|||
return true
|
||||
}
|
||||
|
||||
subject, appErr := a.BuildAccessControlSubject(rctx, userID, roles, channelID)
|
||||
var subject *model.Subject
|
||||
var appErr *model.AppError
|
||||
if rctx.Session().UserId == userID {
|
||||
subject, appErr = a.BuildAccessControlSubjectForSession(rctx, channelID)
|
||||
} else {
|
||||
subject, appErr = a.BuildAccessControlSubject(rctx, userID, roles, channelID)
|
||||
}
|
||||
if appErr != nil {
|
||||
rctx.Logger().Info("Failed to build ABAC subject for file action evaluation",
|
||||
mlog.String("user_id", userID),
|
||||
|
|
|
|||
|
|
@ -1615,19 +1615,24 @@ func (a *App) buildFileDownloadSubject(rctx request.CTX, userID string) (*model.
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
user, err := a.GetUser(userID)
|
||||
if err != nil {
|
||||
rctx.Logger().Warn("Failed to get user for file download permission filtering",
|
||||
mlog.String("user_id", userID),
|
||||
mlog.Err(err),
|
||||
)
|
||||
return nil, err
|
||||
var subject *model.Subject
|
||||
var appErr *model.AppError
|
||||
if rctx.Session().UserId == userID {
|
||||
subject, appErr = a.BuildAccessControlSubjectForSession(rctx, "")
|
||||
} else {
|
||||
user, err := a.GetUser(userID)
|
||||
if err != nil {
|
||||
rctx.Logger().Warn("Failed to get user for file download permission filtering",
|
||||
mlog.String("user_id", userID),
|
||||
mlog.Err(err),
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
// channelID is intentionally empty here: the subject is reused across many
|
||||
// channels in the file-search loop. hasFileDownloadPermission attaches the
|
||||
// channel-scoped role per-evaluation via attachChannelScopedRole.
|
||||
subject, appErr = a.BuildAccessControlSubject(rctx, userID, user.Roles, "")
|
||||
}
|
||||
|
||||
// channelID is intentionally empty here: the subject is reused across many
|
||||
// channels in the file-search loop. hasFileDownloadPermission attaches the
|
||||
// channel-scoped role per-evaluation via attachChannelScopedRole.
|
||||
subject, appErr := a.BuildAccessControlSubject(rctx, userID, user.Roles, "")
|
||||
if appErr != nil {
|
||||
rctx.Logger().Warn("Failed to build ABAC subject for file search filtering",
|
||||
mlog.String("user_id", userID),
|
||||
|
|
|
|||
78
server/channels/app/session_attributes.go
Normal file
78
server/channels/app/session_attributes.go
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/avct/uasurfer"
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost/server/public/shared/request"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/utils"
|
||||
)
|
||||
|
||||
var requestProvidedSessionAttributeFieldNames = []string{
|
||||
model.SessionAttributesPropertyFieldUserAgentPlatform,
|
||||
model.SessionAttributesPropertyFieldUserAgentOS,
|
||||
model.SessionAttributesPropertyFieldUserAgentBrowserName,
|
||||
model.SessionAttributesPropertyFieldUserAgentBrowserVersion,
|
||||
model.SessionAttributesPropertyFieldIPAddress,
|
||||
}
|
||||
|
||||
func (a *App) RefreshRequestProvidedSessionAttributesIfNeeded(rctx request.CTX, r *http.Request) {
|
||||
if !a.Config().FeatureFlags.SessionAttributes {
|
||||
return
|
||||
}
|
||||
|
||||
if l := a.License(); !model.MinimumEnterpriseAdvancedLicense(l) {
|
||||
return
|
||||
}
|
||||
|
||||
session := rctx.Session()
|
||||
if session == nil || session.Id == "" || session.UserId == "" || r == nil {
|
||||
return
|
||||
}
|
||||
if session.Local {
|
||||
return
|
||||
}
|
||||
switch session.Props[model.SessionPropType] {
|
||||
case model.SessionTypeUserAccessToken, model.SessionTypeCloudKey, model.SessionTypeRemoteclusterToken:
|
||||
return
|
||||
}
|
||||
|
||||
attrs := make(map[string]any, len(requestProvidedSessionAttributeFieldNames))
|
||||
for _, name := range requestProvidedSessionAttributeFieldNames {
|
||||
if v := a.getRequestProvidedSessionAttributeByName(r, name); v != "" {
|
||||
attrs[name] = v
|
||||
}
|
||||
}
|
||||
if len(attrs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if err := a.Srv().Store().SessionAttribute().Refresh(session.Id, attrs); err != nil {
|
||||
rctx.Logger().Warn("Failed to refresh session attributes", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) getRequestProvidedSessionAttributeByName(r *http.Request, name string) string {
|
||||
uaStr := r.UserAgent()
|
||||
ua := uasurfer.Parse(uaStr)
|
||||
|
||||
switch name {
|
||||
case model.SessionAttributesPropertyFieldUserAgentPlatform:
|
||||
return getPlatformName(ua, uaStr)
|
||||
case model.SessionAttributesPropertyFieldUserAgentOS:
|
||||
return getOSName(ua, uaStr)
|
||||
case model.SessionAttributesPropertyFieldUserAgentBrowserName:
|
||||
return getBrowserName(ua, uaStr)
|
||||
case model.SessionAttributesPropertyFieldUserAgentBrowserVersion:
|
||||
return getBrowserVersion(ua, uaStr)
|
||||
case model.SessionAttributesPropertyFieldIPAddress:
|
||||
return utils.GetIPAddress(r, a.Config().ServiceSettings.TrustedProxyIPHeader)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
224
server/channels/app/session_attributes_test.go
Normal file
224
server/channels/app/session_attributes_test.go
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/platform/services/cache"
|
||||
)
|
||||
|
||||
const testUserAgentChrome = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36"
|
||||
|
||||
func enableSessionAttributesCollection(t *testing.T, th *TestHelper) {
|
||||
t.Helper()
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
th.ConfigStore.SetReadOnlyFF(false)
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.SessionAttributes = true })
|
||||
th.ConfigStore.SetReadOnlyFF(true)
|
||||
}
|
||||
|
||||
func newSessionAttributesRequest(t *testing.T, userAgent, remoteAddr string) *http.Request {
|
||||
t.Helper()
|
||||
r := httptest.NewRequest(http.MethodGet, "/api/v4/test", nil)
|
||||
if userAgent != "" {
|
||||
r.Header.Set("User-Agent", userAgent)
|
||||
}
|
||||
if remoteAddr != "" {
|
||||
r.RemoteAddr = remoteAddr
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func sessionAttributeValuesByFieldName(t *testing.T, th *TestHelper, sessionID string) map[string]string {
|
||||
t.Helper()
|
||||
attrs, err := th.App.Srv().Store().SessionAttribute().Get(sessionID)
|
||||
if errors.Is(err, cache.ErrKeyNotFound) {
|
||||
return map[string]string{}
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
result := make(map[string]string, len(attrs))
|
||||
for k, v := range attrs {
|
||||
s, ok := v.(string)
|
||||
require.True(t, ok, "expected string for session attribute %q", k)
|
||||
result[k] = s
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func TestRefreshRequestProvidedSessionAttributesIfNeeded(t *testing.T) {
|
||||
t.Run("skips when feature flag is disabled", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic(t)
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
|
||||
session, appErr := th.App.CreateSession(th.Context, &model.Session{UserId: th.BasicUser.Id, Props: model.StringMap{}})
|
||||
require.Nil(t, appErr)
|
||||
rctx := th.Context.WithSession(session)
|
||||
|
||||
r := newSessionAttributesRequest(t, testUserAgentChrome, "192.0.2.10:1234")
|
||||
th.App.RefreshRequestProvidedSessionAttributesIfNeeded(rctx, r)
|
||||
|
||||
require.Empty(t, sessionAttributeValuesByFieldName(t, th, session.Id))
|
||||
})
|
||||
|
||||
t.Run("skips when license is missing", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic(t)
|
||||
th.ConfigStore.SetReadOnlyFF(false)
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.SessionAttributes = true })
|
||||
th.ConfigStore.SetReadOnlyFF(true)
|
||||
|
||||
session, appErr := th.App.CreateSession(th.Context, &model.Session{UserId: th.BasicUser.Id, Props: model.StringMap{}})
|
||||
require.Nil(t, appErr)
|
||||
rctx := th.Context.WithSession(session)
|
||||
|
||||
r := newSessionAttributesRequest(t, testUserAgentChrome, "192.0.2.10:1234")
|
||||
th.App.RefreshRequestProvidedSessionAttributesIfNeeded(rctx, r)
|
||||
|
||||
require.Empty(t, sessionAttributeValuesByFieldName(t, th, session.Id))
|
||||
})
|
||||
|
||||
t.Run("skips when session is local", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic(t)
|
||||
enableSessionAttributesCollection(t, th)
|
||||
|
||||
session := &model.Session{Id: model.NewId(), UserId: th.BasicUser.Id, Local: true, Props: model.StringMap{}}
|
||||
rctx := th.Context.WithSession(session)
|
||||
|
||||
r := newSessionAttributesRequest(t, testUserAgentChrome, "192.0.2.10:1234")
|
||||
th.App.RefreshRequestProvidedSessionAttributesIfNeeded(rctx, r)
|
||||
|
||||
require.Empty(t, sessionAttributeValuesByFieldName(t, th, session.Id))
|
||||
})
|
||||
|
||||
t.Run("skips token-based session types", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic(t)
|
||||
enableSessionAttributesCollection(t, th)
|
||||
|
||||
tokenTypes := []string{
|
||||
model.SessionTypeUserAccessToken,
|
||||
model.SessionTypeCloudKey,
|
||||
model.SessionTypeRemoteclusterToken,
|
||||
}
|
||||
|
||||
for _, sessionType := range tokenTypes {
|
||||
t.Run(sessionType, func(t *testing.T) {
|
||||
session, appErr := th.App.CreateSession(th.Context, &model.Session{
|
||||
UserId: th.BasicUser.Id,
|
||||
Props: model.StringMap{model.SessionPropType: sessionType},
|
||||
})
|
||||
require.Nil(t, appErr)
|
||||
rctx := th.Context.WithSession(session)
|
||||
|
||||
r := newSessionAttributesRequest(t, testUserAgentChrome, "192.0.2.10:1234")
|
||||
th.App.RefreshRequestProvidedSessionAttributesIfNeeded(rctx, r)
|
||||
|
||||
require.Empty(t, sessionAttributeValuesByFieldName(t, th, session.Id))
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("skips when session id is empty", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic(t)
|
||||
enableSessionAttributesCollection(t, th)
|
||||
|
||||
rctx := th.Context.WithSession(&model.Session{UserId: th.BasicUser.Id, Props: model.StringMap{}})
|
||||
|
||||
r := newSessionAttributesRequest(t, testUserAgentChrome, "192.0.2.10:1234")
|
||||
th.App.RefreshRequestProvidedSessionAttributesIfNeeded(rctx, r)
|
||||
})
|
||||
|
||||
t.Run("skips when request is nil", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic(t)
|
||||
enableSessionAttributesCollection(t, th)
|
||||
|
||||
session, appErr := th.App.CreateSession(th.Context, &model.Session{UserId: th.BasicUser.Id, Props: model.StringMap{}})
|
||||
require.Nil(t, appErr)
|
||||
rctx := th.Context.WithSession(session)
|
||||
|
||||
th.App.RefreshRequestProvidedSessionAttributesIfNeeded(rctx, nil)
|
||||
|
||||
require.Empty(t, sessionAttributeValuesByFieldName(t, th, session.Id))
|
||||
})
|
||||
|
||||
t.Run("creates session attributes when none exist", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic(t)
|
||||
enableSessionAttributesCollection(t, th)
|
||||
|
||||
session, appErr := th.App.CreateSession(th.Context, &model.Session{UserId: th.BasicUser.Id, Props: model.StringMap{}})
|
||||
require.Nil(t, appErr)
|
||||
rctx := th.Context.WithSession(session)
|
||||
|
||||
r := newSessionAttributesRequest(t, testUserAgentChrome, "192.0.2.10:1234")
|
||||
th.App.RefreshRequestProvidedSessionAttributesIfNeeded(rctx, r)
|
||||
|
||||
valuesByName := sessionAttributeValuesByFieldName(t, th, session.Id)
|
||||
assert.Equal(t, "Macintosh", valuesByName[model.SessionAttributesPropertyFieldUserAgentPlatform])
|
||||
assert.Equal(t, "Mac OS", valuesByName[model.SessionAttributesPropertyFieldUserAgentOS])
|
||||
assert.Equal(t, "Chrome", valuesByName[model.SessionAttributesPropertyFieldUserAgentBrowserName])
|
||||
assert.Equal(t, "60.0.3112", valuesByName[model.SessionAttributesPropertyFieldUserAgentBrowserVersion])
|
||||
assert.Equal(t, "192.0.2.10", valuesByName[model.SessionAttributesPropertyFieldIPAddress])
|
||||
})
|
||||
|
||||
t.Run("each call overwrites cached attributes with the latest request values", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic(t)
|
||||
enableSessionAttributesCollection(t, th)
|
||||
|
||||
session, appErr := th.App.CreateSession(th.Context, &model.Session{UserId: th.BasicUser.Id, Props: model.StringMap{}})
|
||||
require.Nil(t, appErr)
|
||||
rctx := th.Context.WithSession(session)
|
||||
|
||||
firstRequest := newSessionAttributesRequest(t, testUserAgentChrome, "192.0.2.10:1234")
|
||||
th.App.RefreshRequestProvidedSessionAttributesIfNeeded(rctx, firstRequest)
|
||||
|
||||
initial := sessionAttributeValuesByFieldName(t, th, session.Id)
|
||||
assert.Equal(t, "192.0.2.10", initial[model.SessionAttributesPropertyFieldIPAddress])
|
||||
assert.Equal(t, "Chrome", initial[model.SessionAttributesPropertyFieldUserAgentBrowserName])
|
||||
|
||||
const otherUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0"
|
||||
secondRequest := newSessionAttributesRequest(t, otherUserAgent, "203.0.113.42:1234")
|
||||
th.App.RefreshRequestProvidedSessionAttributesIfNeeded(rctx, secondRequest)
|
||||
|
||||
after := sessionAttributeValuesByFieldName(t, th, session.Id)
|
||||
assert.Equal(t, "203.0.113.42", after[model.SessionAttributesPropertyFieldIPAddress])
|
||||
assert.Equal(t, "Firefox", after[model.SessionAttributesPropertyFieldUserAgentBrowserName])
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetRequestProvidedSessionAttributeByName(t *testing.T) {
|
||||
th := Setup(t)
|
||||
|
||||
r := newSessionAttributesRequest(t, testUserAgentChrome, "198.51.100.7:5678")
|
||||
|
||||
t.Run("returns user agent platform", func(t *testing.T) {
|
||||
assert.Equal(t, "Macintosh", th.App.getRequestProvidedSessionAttributeByName(r, model.SessionAttributesPropertyFieldUserAgentPlatform))
|
||||
})
|
||||
|
||||
t.Run("returns user agent OS", func(t *testing.T) {
|
||||
assert.Equal(t, "Mac OS", th.App.getRequestProvidedSessionAttributeByName(r, model.SessionAttributesPropertyFieldUserAgentOS))
|
||||
})
|
||||
|
||||
t.Run("returns user agent browser name", func(t *testing.T) {
|
||||
assert.Equal(t, "Chrome", th.App.getRequestProvidedSessionAttributeByName(r, model.SessionAttributesPropertyFieldUserAgentBrowserName))
|
||||
})
|
||||
|
||||
t.Run("returns user agent browser version", func(t *testing.T) {
|
||||
assert.Equal(t, "60.0.3112", th.App.getRequestProvidedSessionAttributeByName(r, model.SessionAttributesPropertyFieldUserAgentBrowserVersion))
|
||||
})
|
||||
|
||||
t.Run("returns IP address", func(t *testing.T) {
|
||||
assert.Equal(t, "198.51.100.7", th.App.getRequestProvidedSessionAttributeByName(r, model.SessionAttributesPropertyFieldIPAddress))
|
||||
})
|
||||
|
||||
t.Run("returns empty string for unknown name", func(t *testing.T) {
|
||||
assert.Empty(t, th.App.getRequestProvidedSessionAttributeByName(r, "unknown_attribute"))
|
||||
})
|
||||
}
|
||||
|
|
@ -82,6 +82,9 @@ const (
|
|||
|
||||
TemporaryPostCacheSize = 10000
|
||||
TemporaryPostCacheMinutes = 60
|
||||
|
||||
SessionAttributeCacheSize = model.SessionCacheSize
|
||||
SessionAttributeCacheSec = 30
|
||||
)
|
||||
|
||||
var clearCacheMessageData = []byte("")
|
||||
|
|
@ -152,6 +155,9 @@ type LocalCacheStore struct {
|
|||
|
||||
temporaryPost LocalCacheTemporaryPostStore
|
||||
temporaryPostCache cache.Cache
|
||||
|
||||
sessionAttribute LocalCacheSessionAttributeStore
|
||||
sessionAttributeCache cache.Cache
|
||||
}
|
||||
|
||||
func NewLocalCacheLayer(baseStore store.Store, metrics einterfaces.MetricsInterface, cluster einterfaces.ClusterInterface, cacheProvider cache.Provider, logger mlog.LoggerIFace) (localCacheStore LocalCacheStore, err error) {
|
||||
|
|
@ -456,6 +462,17 @@ func NewLocalCacheLayer(baseStore store.Store, metrics einterfaces.MetricsInterf
|
|||
}
|
||||
localCacheStore.temporaryPost = LocalCacheTemporaryPostStore{TemporaryPostStore: baseStore.TemporaryPost(), rootStore: &localCacheStore}
|
||||
|
||||
// Session Attributes
|
||||
if localCacheStore.sessionAttributeCache, err = cacheProvider.NewCache(&cache.CacheOptions{
|
||||
Size: SessionAttributeCacheSize,
|
||||
Name: "SessionAttribute",
|
||||
DefaultExpiry: SessionAttributeCacheSec * time.Second,
|
||||
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForSessionAttributes,
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
localCacheStore.sessionAttribute = LocalCacheSessionAttributeStore{SessionAttributeStore: baseStore.SessionAttribute(), rootStore: &localCacheStore}
|
||||
|
||||
if cluster != nil {
|
||||
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForReactions, localCacheStore.reaction.handleClusterInvalidateReaction)
|
||||
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForRoles, localCacheStore.role.handleClusterInvalidateRole)
|
||||
|
|
@ -485,6 +502,7 @@ func NewLocalCacheLayer(baseStore store.Store, metrics einterfaces.MetricsInterf
|
|||
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForContentFlagging, localCacheStore.contentFlagging.handleClusterInvalidateContentFlagging)
|
||||
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForReadReceipts, localCacheStore.readReceipt.handleClusterInvalidateReadReceipts)
|
||||
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForTemporaryPosts, localCacheStore.temporaryPost.handleClusterInvalidateTemporaryPosts)
|
||||
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForSessionAttributes, localCacheStore.sessionAttribute.handleClusterInvalidateSessionAttributes)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -549,6 +567,10 @@ func (s LocalCacheStore) TemporaryPost() store.TemporaryPostStore {
|
|||
return s.temporaryPost
|
||||
}
|
||||
|
||||
func (s LocalCacheStore) SessionAttribute() store.SessionAttributeStore {
|
||||
return &s.sessionAttribute
|
||||
}
|
||||
|
||||
func (s LocalCacheStore) DropAllTables() {
|
||||
s.Invalidate()
|
||||
s.Store.DropAllTables()
|
||||
|
|
@ -687,6 +709,7 @@ func (s *LocalCacheStore) Invalidate() {
|
|||
s.doClearCacheCluster(s.readReceiptCache)
|
||||
s.doClearCacheCluster(s.readReceiptPostReadersCache)
|
||||
s.doClearCacheCluster(s.temporaryPostCache)
|
||||
s.doClearCacheCluster(s.sessionAttributeCache)
|
||||
}
|
||||
|
||||
// allocateCacheTargets is used to fill target value types
|
||||
|
|
|
|||
|
|
@ -204,6 +204,9 @@ func getMockStore(t *testing.T) *mocks.Store {
|
|||
mockContentFlaggingStore := mocks.ContentFlaggingStore{}
|
||||
mockStore.On("ContentFlagging").Return(&mockContentFlaggingStore)
|
||||
|
||||
mockSessionAttributeStore := mocks.SessionAttributeStore{}
|
||||
mockStore.On("SessionAttribute").Return(&mockSessionAttributeStore)
|
||||
|
||||
mockReadReceiptStore := &mocks.ReadReceiptStore{}
|
||||
mockStore.On("ReadReceipt").Return(mockReadReceiptStore)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package localcachelayer
|
||||
|
||||
import (
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/store"
|
||||
)
|
||||
|
||||
type sessionAttributeEntry struct {
|
||||
Attrs map[string]any
|
||||
}
|
||||
|
||||
type LocalCacheSessionAttributeStore struct {
|
||||
store.SessionAttributeStore
|
||||
rootStore *LocalCacheStore
|
||||
}
|
||||
|
||||
func (s *LocalCacheSessionAttributeStore) handleClusterInvalidateSessionAttributes(_ *model.ClusterMessage) {
|
||||
if err := s.rootStore.sessionAttributeCache.Purge(); err != nil {
|
||||
s.rootStore.logger.Warn("failed to purge session attribute cache", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LocalCacheSessionAttributeStore) Refresh(sessionID string, attrs map[string]any) error {
|
||||
if err := s.rootStore.sessionAttributeCache.SetWithDefaultExpiry(sessionID, &sessionAttributeEntry{Attrs: attrs}); err != nil {
|
||||
s.rootStore.logger.Warn("failed to set session attribute cache", mlog.Err(err))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *LocalCacheSessionAttributeStore) Get(sessionID string) (map[string]any, error) {
|
||||
var entry *sessionAttributeEntry
|
||||
if err := s.rootStore.sessionAttributeCache.Get(sessionID, &entry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return entry.Attrs, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package localcachelayer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost/server/v8/platform/services/cache"
|
||||
)
|
||||
|
||||
func TestSessionAttributeStoreCache(t *testing.T) {
|
||||
logger := mlog.CreateConsoleTestLogger(t)
|
||||
|
||||
t.Run("Refresh writes attributes that Get returns", func(t *testing.T) {
|
||||
mockStore := getMockStore(t)
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
|
||||
require.NoError(t, err)
|
||||
|
||||
sessionID := model.NewId()
|
||||
err = cachedStore.SessionAttribute().Refresh(sessionID, map[string]any{
|
||||
model.SessionAttributesPropertyFieldUserAgentBrowserName: "Chrome",
|
||||
model.SessionAttributesPropertyFieldIPAddress: "192.0.2.10",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err := cachedStore.SessionAttribute().Get(sessionID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "Chrome", got[model.SessionAttributesPropertyFieldUserAgentBrowserName])
|
||||
require.Equal(t, "192.0.2.10", got[model.SessionAttributesPropertyFieldIPAddress])
|
||||
})
|
||||
|
||||
t.Run("Refresh overwrites existing attributes", func(t *testing.T) {
|
||||
mockStore := getMockStore(t)
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
|
||||
require.NoError(t, err)
|
||||
|
||||
sessionID := model.NewId()
|
||||
require.NoError(t, cachedStore.SessionAttribute().Refresh(sessionID, map[string]any{
|
||||
model.SessionAttributesPropertyFieldUserAgentBrowserName: "Chrome",
|
||||
}))
|
||||
|
||||
require.NoError(t, cachedStore.SessionAttribute().Refresh(sessionID, map[string]any{
|
||||
model.SessionAttributesPropertyFieldUserAgentBrowserName: "Firefox",
|
||||
}))
|
||||
|
||||
got, err := cachedStore.SessionAttribute().Get(sessionID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "Firefox", got[model.SessionAttributesPropertyFieldUserAgentBrowserName])
|
||||
})
|
||||
|
||||
t.Run("Get on missing session returns ErrKeyNotFound", func(t *testing.T) {
|
||||
mockStore := getMockStore(t)
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err := cachedStore.SessionAttribute().Get(model.NewId())
|
||||
require.ErrorIs(t, err, cache.ErrKeyNotFound)
|
||||
require.Nil(t, got)
|
||||
})
|
||||
|
||||
t.Run("cluster invalidation with clear-cache marker purges every entry", func(t *testing.T) {
|
||||
mockStore := getMockStore(t)
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
|
||||
require.NoError(t, err)
|
||||
|
||||
sessionA := model.NewId()
|
||||
sessionB := model.NewId()
|
||||
require.NoError(t, cachedStore.SessionAttribute().Refresh(sessionA, map[string]any{
|
||||
model.SessionAttributesPropertyFieldIPAddress: "192.0.2.10",
|
||||
}))
|
||||
require.NoError(t, cachedStore.SessionAttribute().Refresh(sessionB, map[string]any{
|
||||
model.SessionAttributesPropertyFieldIPAddress: "203.0.113.42",
|
||||
}))
|
||||
|
||||
cachedStore.sessionAttribute.handleClusterInvalidateSessionAttributes(&model.ClusterMessage{
|
||||
Event: model.ClusterEventInvalidateCacheForSessionAttributes,
|
||||
Data: clearCacheMessageData,
|
||||
})
|
||||
|
||||
_, err = cachedStore.SessionAttribute().Get(sessionA)
|
||||
require.ErrorIs(t, err, cache.ErrKeyNotFound)
|
||||
_, err = cachedStore.SessionAttribute().Get(sessionB)
|
||||
require.ErrorIs(t, err, cache.ErrKeyNotFound)
|
||||
})
|
||||
}
|
||||
|
|
@ -65,6 +65,7 @@ type RetryLayer struct {
|
|||
ScheduledPostStore store.ScheduledPostStore
|
||||
SchemeStore store.SchemeStore
|
||||
SessionStore store.SessionStore
|
||||
SessionAttributeStore store.SessionAttributeStore
|
||||
SharedChannelStore store.SharedChannelStore
|
||||
StatusStore store.StatusStore
|
||||
SystemStore store.SystemStore
|
||||
|
|
@ -261,6 +262,10 @@ func (s *RetryLayer) Session() store.SessionStore {
|
|||
return s.SessionStore
|
||||
}
|
||||
|
||||
func (s *RetryLayer) SessionAttribute() store.SessionAttributeStore {
|
||||
return s.SessionAttributeStore
|
||||
}
|
||||
|
||||
func (s *RetryLayer) SharedChannel() store.SharedChannelStore {
|
||||
return s.SharedChannelStore
|
||||
}
|
||||
|
|
@ -542,6 +547,11 @@ type RetryLayerSessionStore struct {
|
|||
Root *RetryLayer
|
||||
}
|
||||
|
||||
type RetryLayerSessionAttributeStore struct {
|
||||
store.SessionAttributeStore
|
||||
Root *RetryLayer
|
||||
}
|
||||
|
||||
type RetryLayerSharedChannelStore struct {
|
||||
store.SharedChannelStore
|
||||
Root *RetryLayer
|
||||
|
|
@ -13073,6 +13083,48 @@ func (s *RetryLayerSessionStore) UpdateRoles(userID string, roles string) (strin
|
|||
|
||||
}
|
||||
|
||||
func (s *RetryLayerSessionAttributeStore) Get(sessionID string) (map[string]any, error) {
|
||||
|
||||
tries := 0
|
||||
for {
|
||||
result, err := s.SessionAttributeStore.Get(sessionID)
|
||||
if err == nil {
|
||||
return result, nil
|
||||
}
|
||||
if !isRepeatableError(err) {
|
||||
return result, err
|
||||
}
|
||||
tries++
|
||||
if tries >= 3 {
|
||||
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
|
||||
return result, err
|
||||
}
|
||||
timepkg.Sleep(100 * timepkg.Millisecond)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (s *RetryLayerSessionAttributeStore) Refresh(sessionID string, attrs map[string]any) error {
|
||||
|
||||
tries := 0
|
||||
for {
|
||||
err := s.SessionAttributeStore.Refresh(sessionID, attrs)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if !isRepeatableError(err) {
|
||||
return err
|
||||
}
|
||||
tries++
|
||||
if tries >= 3 {
|
||||
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
|
||||
return err
|
||||
}
|
||||
timepkg.Sleep(100 * timepkg.Millisecond)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (s *RetryLayerSharedChannelStore) Delete(channelID string) (bool, error) {
|
||||
|
||||
tries := 0
|
||||
|
|
@ -18907,6 +18959,7 @@ func New(childStore store.Store) *RetryLayer {
|
|||
newStore.ScheduledPostStore = &RetryLayerScheduledPostStore{ScheduledPostStore: childStore.ScheduledPost(), Root: &newStore}
|
||||
newStore.SchemeStore = &RetryLayerSchemeStore{SchemeStore: childStore.Scheme(), Root: &newStore}
|
||||
newStore.SessionStore = &RetryLayerSessionStore{SessionStore: childStore.Session(), Root: &newStore}
|
||||
newStore.SessionAttributeStore = &RetryLayerSessionAttributeStore{SessionAttributeStore: childStore.SessionAttribute(), Root: &newStore}
|
||||
newStore.SharedChannelStore = &RetryLayerSharedChannelStore{SharedChannelStore: childStore.SharedChannel(), Root: &newStore}
|
||||
newStore.StatusStore = &RetryLayerStatusStore{StatusStore: childStore.Status(), Root: &newStore}
|
||||
newStore.SystemStore = &RetryLayerSystemStore{SystemStore: childStore.System(), Root: &newStore}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ func genStore() *mocks.Store {
|
|||
mock.On("Role").Return(&mocks.RoleStore{})
|
||||
mock.On("Scheme").Return(&mocks.SchemeStore{})
|
||||
mock.On("Session").Return(&mocks.SessionStore{})
|
||||
mock.On("SessionAttribute").Return(&mocks.SessionAttributeStore{})
|
||||
mock.On("Status").Return(&mocks.StatusStore{})
|
||||
mock.On("System").Return(&mocks.SystemStore{})
|
||||
mock.On("Team").Return(&mocks.TeamStore{})
|
||||
|
|
|
|||
28
server/channels/store/sqlstore/session_attribute_store.go
Normal file
28
server/channels/store/sqlstore/session_attribute_store.go
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"github.com/mattermost/mattermost/server/v8/channels/store"
|
||||
)
|
||||
|
||||
// SqlSessionAttributeStore is a no-op SessionAttributeStore: session
|
||||
// attributes are never persisted to SQL. The localcachelayer wraps this and
|
||||
// is the source of truth at runtime; the DB-backed shim exists only to
|
||||
// satisfy the store.Store interface.
|
||||
type SqlSessionAttributeStore struct {
|
||||
*SqlStore
|
||||
}
|
||||
|
||||
func newSqlSessionAttributeStore(sqlStore *SqlStore) store.SessionAttributeStore {
|
||||
return &SqlSessionAttributeStore{SqlStore: sqlStore}
|
||||
}
|
||||
|
||||
func (s *SqlSessionAttributeStore) Refresh(_ string, _ map[string]any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SqlSessionAttributeStore) Get(_ string) (map[string]any, error) {
|
||||
return map[string]any{}, nil
|
||||
}
|
||||
|
|
@ -113,6 +113,7 @@ type SqlStoreStores struct {
|
|||
propertyValue store.PropertyValueStore
|
||||
accessControlPolicy store.AccessControlPolicyStore
|
||||
Attributes store.AttributesStore
|
||||
sessionAttribute store.SessionAttributeStore
|
||||
autotranslation store.AutoTranslationStore
|
||||
ContentFlagging store.ContentFlaggingStore
|
||||
recap store.RecapStore
|
||||
|
|
@ -301,6 +302,7 @@ func New(settings model.SqlSettings, logger mlog.LoggerIFace, metrics einterface
|
|||
store.stores.propertyValue = newPropertyValueStore(store)
|
||||
store.stores.accessControlPolicy = newSqlAccessControlPolicyStore(store, metrics)
|
||||
store.stores.Attributes = newSqlAttributesStore(store, metrics)
|
||||
store.stores.sessionAttribute = newSqlSessionAttributeStore(store)
|
||||
store.stores.autotranslation = newSqlAutoTranslationStore(store)
|
||||
store.stores.ContentFlagging = newContentFlaggingStore(store)
|
||||
store.stores.recap = newSqlRecapStore(store)
|
||||
|
|
@ -943,6 +945,10 @@ func (ss *SqlStore) Attributes() store.AttributesStore {
|
|||
return ss.stores.Attributes
|
||||
}
|
||||
|
||||
func (ss *SqlStore) SessionAttribute() store.SessionAttributeStore {
|
||||
return ss.stores.sessionAttribute
|
||||
}
|
||||
|
||||
func (ss *SqlStore) AutoTranslation() store.AutoTranslationStore {
|
||||
return ss.stores.autotranslation
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ type Store interface {
|
|||
PropertyValue() PropertyValueStore
|
||||
AccessControlPolicy() AccessControlPolicyStore
|
||||
Attributes() AttributesStore
|
||||
SessionAttribute() SessionAttributeStore
|
||||
AutoTranslation() AutoTranslationStore
|
||||
GetSchemaDefinition() (*model.SupportPacketDatabaseSchema, error)
|
||||
ContentFlagging() ContentFlaggingStore
|
||||
|
|
@ -1226,6 +1227,11 @@ type AttributesStore interface {
|
|||
GetChannelMembersToRemove(rctx request.CTX, channelID string, opts model.SubjectSearchOptions) ([]*model.ChannelMember, error)
|
||||
}
|
||||
|
||||
type SessionAttributeStore interface {
|
||||
Refresh(sessionID string, attrs map[string]any) error
|
||||
Get(sessionID string) (map[string]any, error)
|
||||
}
|
||||
|
||||
type AutoTranslationStore interface {
|
||||
IsUserEnabled(userID, channelID string) (bool, error)
|
||||
GetUserLanguage(userID, channelID string) (string, error)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
// Code generated by mockery v2.53.4. DO NOT EDIT.
|
||||
|
||||
// Regenerate this file using `make store-mocks`.
|
||||
|
||||
package mocks
|
||||
|
||||
import mock "github.com/stretchr/testify/mock"
|
||||
|
||||
// SessionAttributeStore is an autogenerated mock type for the SessionAttributeStore type
|
||||
type SessionAttributeStore struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Get provides a mock function with given fields: sessionID
|
||||
func (_m *SessionAttributeStore) Get(sessionID string) (map[string]interface{}, error) {
|
||||
ret := _m.Called(sessionID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Get")
|
||||
}
|
||||
|
||||
var r0 map[string]interface{}
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string) (map[string]interface{}, error)); ok {
|
||||
return rf(sessionID)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string) map[string]interface{}); ok {
|
||||
r0 = rf(sessionID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(map[string]interface{})
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(sessionID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Refresh provides a mock function with given fields: sessionID, attrs
|
||||
func (_m *SessionAttributeStore) Refresh(sessionID string, attrs map[string]interface{}) error {
|
||||
ret := _m.Called(sessionID, attrs)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Refresh")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, map[string]interface{}) error); ok {
|
||||
r0 = rf(sessionID, attrs)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// NewSessionAttributeStore creates a new instance of SessionAttributeStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewSessionAttributeStore(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *SessionAttributeStore {
|
||||
mock := &SessionAttributeStore{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
|
|
@ -1235,6 +1235,26 @@ func (_m *Store) Session() store.SessionStore {
|
|||
return r0
|
||||
}
|
||||
|
||||
// SessionAttribute provides a mock function with no fields
|
||||
func (_m *Store) SessionAttribute() store.SessionAttributeStore {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for SessionAttribute")
|
||||
}
|
||||
|
||||
var r0 store.SessionAttributeStore
|
||||
if rf, ok := ret.Get(0).(func() store.SessionAttributeStore); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(store.SessionAttributeStore)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SharedChannel provides a mock function with no fields
|
||||
func (_m *Store) SharedChannel() store.SharedChannelStore {
|
||||
ret := _m.Called()
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ type Store struct {
|
|||
PropertyValueStore mocks.PropertyValueStore
|
||||
AccessControlPolicyStore mocks.AccessControlPolicyStore
|
||||
AttributesStore mocks.AttributesStore
|
||||
SessionAttributeStore mocks.SessionAttributeStore
|
||||
AutoTranslationStore mocks.AutoTranslationStore
|
||||
ContentFlaggingStore mocks.ContentFlaggingStore
|
||||
RecapStore mocks.RecapStore
|
||||
|
|
@ -168,6 +169,9 @@ func (s *Store) AccessControlPolicy() store.AccessControlPolicyStore {
|
|||
func (s *Store) Attributes() store.AttributesStore {
|
||||
return &s.AttributesStore
|
||||
}
|
||||
func (s *Store) SessionAttribute() store.SessionAttributeStore {
|
||||
return &s.SessionAttributeStore
|
||||
}
|
||||
func (s *Store) AutoTranslation() store.AutoTranslationStore {
|
||||
return &s.AutoTranslationStore
|
||||
}
|
||||
|
|
@ -245,6 +249,7 @@ func (s *Store) AssertExpectations(t mock.TestingT) bool {
|
|||
&s.ScheduledPostStore,
|
||||
&s.AccessControlPolicyStore,
|
||||
&s.AttributesStore,
|
||||
&s.SessionAttributeStore,
|
||||
&s.AutoTranslationStore,
|
||||
&s.ContentFlaggingStore,
|
||||
&s.RecapStore,
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ type TimerLayer struct {
|
|||
ScheduledPostStore store.ScheduledPostStore
|
||||
SchemeStore store.SchemeStore
|
||||
SessionStore store.SessionStore
|
||||
SessionAttributeStore store.SessionAttributeStore
|
||||
SharedChannelStore store.SharedChannelStore
|
||||
StatusStore store.StatusStore
|
||||
SystemStore store.SystemStore
|
||||
|
|
@ -260,6 +261,10 @@ func (s *TimerLayer) Session() store.SessionStore {
|
|||
return s.SessionStore
|
||||
}
|
||||
|
||||
func (s *TimerLayer) SessionAttribute() store.SessionAttributeStore {
|
||||
return s.SessionAttributeStore
|
||||
}
|
||||
|
||||
func (s *TimerLayer) SharedChannel() store.SharedChannelStore {
|
||||
return s.SharedChannelStore
|
||||
}
|
||||
|
|
@ -541,6 +546,11 @@ type TimerLayerSessionStore struct {
|
|||
Root *TimerLayer
|
||||
}
|
||||
|
||||
type TimerLayerSessionAttributeStore struct {
|
||||
store.SessionAttributeStore
|
||||
Root *TimerLayer
|
||||
}
|
||||
|
||||
type TimerLayerSharedChannelStore struct {
|
||||
store.SharedChannelStore
|
||||
Root *TimerLayer
|
||||
|
|
@ -10351,6 +10361,38 @@ func (s *TimerLayerSessionStore) UpdateRoles(userID string, roles string) (strin
|
|||
return result, err
|
||||
}
|
||||
|
||||
func (s *TimerLayerSessionAttributeStore) Get(sessionID string) (map[string]any, error) {
|
||||
start := time.Now()
|
||||
|
||||
result, err := s.SessionAttributeStore.Get(sessionID)
|
||||
|
||||
elapsed := float64(time.Since(start)) / float64(time.Second)
|
||||
if s.Root.Metrics != nil {
|
||||
success := "false"
|
||||
if err == nil {
|
||||
success = "true"
|
||||
}
|
||||
s.Root.Metrics.ObserveStoreMethodDuration("SessionAttributeStore.Get", success, elapsed)
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *TimerLayerSessionAttributeStore) Refresh(sessionID string, attrs map[string]any) error {
|
||||
start := time.Now()
|
||||
|
||||
err := s.SessionAttributeStore.Refresh(sessionID, attrs)
|
||||
|
||||
elapsed := float64(time.Since(start)) / float64(time.Second)
|
||||
if s.Root.Metrics != nil {
|
||||
success := "false"
|
||||
if err == nil {
|
||||
success = "true"
|
||||
}
|
||||
s.Root.Metrics.ObserveStoreMethodDuration("SessionAttributeStore.Refresh", success, elapsed)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *TimerLayerSharedChannelStore) Delete(channelID string) (bool, error) {
|
||||
start := time.Now()
|
||||
|
||||
|
|
@ -14956,6 +14998,7 @@ func New(childStore store.Store, metrics einterfaces.MetricsInterface) *TimerLay
|
|||
newStore.ScheduledPostStore = &TimerLayerScheduledPostStore{ScheduledPostStore: childStore.ScheduledPost(), Root: &newStore}
|
||||
newStore.SchemeStore = &TimerLayerSchemeStore{SchemeStore: childStore.Scheme(), Root: &newStore}
|
||||
newStore.SessionStore = &TimerLayerSessionStore{SessionStore: childStore.Session(), Root: &newStore}
|
||||
newStore.SessionAttributeStore = &TimerLayerSessionAttributeStore{SessionAttributeStore: childStore.SessionAttribute(), Root: &newStore}
|
||||
newStore.SharedChannelStore = &TimerLayerSharedChannelStore{SharedChannelStore: childStore.SharedChannel(), Root: &newStore}
|
||||
newStore.StatusStore = &TimerLayerStatusStore{StatusStore: childStore.Status(), Root: &newStore}
|
||||
newStore.SystemStore = &TimerLayerSystemStore{SystemStore: childStore.System(), Root: &newStore}
|
||||
|
|
|
|||
|
|
@ -355,6 +355,10 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
if c.Err == nil {
|
||||
c.App.RefreshRequestProvidedSessionAttributesIfNeeded(c.AppContext, r)
|
||||
}
|
||||
|
||||
if c.Err == nil {
|
||||
h.HandleFunc(c, w, r)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -536,6 +536,7 @@ func New(ps *platform.PlatformService, driver, dataSource string) *MetricsInterf
|
|||
model.ClusterEventInvalidateCacheForPostsUsage,
|
||||
model.ClusterEventInvalidateCacheForTeams,
|
||||
model.ClusterEventInvalidateCacheForContentFlagging,
|
||||
model.ClusterEventInvalidateCacheForSessionAttributes,
|
||||
model.ClusterEventClearSessionCacheForAllUsers,
|
||||
model.ClusterEventInstallPlugin,
|
||||
model.ClusterEventRemovePlugin,
|
||||
|
|
|
|||
|
|
@ -5258,6 +5258,10 @@
|
|||
"id": "app.access_control.build_subject.group_id.app_error",
|
||||
"translation": "Failed to retrieve the access control attribute group."
|
||||
},
|
||||
{
|
||||
"id": "app.access_control.build_subject_for_session.get_session_attributes.app_error",
|
||||
"translation": "Failed to retrieve the session attributes."
|
||||
},
|
||||
{
|
||||
"id": "app.access_control.get_channel_role.app_error",
|
||||
"translation": "Unable to get channel role for the user. Please try again."
|
||||
|
|
@ -10850,6 +10854,10 @@
|
|||
"id": "model.access_policy.is_valid.scope_id_without_scope.app_error",
|
||||
"translation": "Scope ID cannot be set without a scope."
|
||||
},
|
||||
{
|
||||
"id": "model.access_policy.is_valid.session_attribute_on_membership.app_error",
|
||||
"translation": "Membership rules cannot reference session attributes (user.session). Use a permission policy instead."
|
||||
},
|
||||
{
|
||||
"id": "model.access_policy.is_valid.type.app_error",
|
||||
"translation": "Invalid policy type."
|
||||
|
|
|
|||
|
|
@ -378,6 +378,9 @@ func (p *AccessControlPolicy) accessPolicyVersionV0_3() *AppError {
|
|||
return NewAppError("AccessControlPolicy.IsValid", "model.access_policy.is_valid.actions.app_error", nil, fmt.Sprintf("unrecognized action: %s", action), 400)
|
||||
}
|
||||
}
|
||||
if slices.Contains(rule.Actions, AccessControlPolicyActionMembership) && strings.Contains(rule.Expression, "user.session") {
|
||||
return NewAppError("AccessControlPolicy.IsValid", "model.access_policy.is_valid.session_attribute_on_membership.app_error", nil, "", 400)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -494,6 +494,91 @@ func TestAccessPolicyVersionV0_3(t *testing.T) {
|
|||
require.NotNil(t, err)
|
||||
require.Equal(t, "model.access_policy.is_valid.rules_imports.app_error", err.Id)
|
||||
})
|
||||
|
||||
t.Run("membership rule rejects session attribute reference", func(t *testing.T) {
|
||||
policy := &AccessControlPolicy{
|
||||
ID: NewId(),
|
||||
Type: AccessControlPolicyTypeParent,
|
||||
Name: "Parent",
|
||||
Revision: 0,
|
||||
Version: AccessControlPolicyVersionV0_3,
|
||||
Rules: []AccessControlPolicyRule{{
|
||||
Actions: []string{AccessControlPolicyActionMembership},
|
||||
Expression: "user.session.ip_address == \"10.0.0.1\"",
|
||||
}},
|
||||
}
|
||||
err := policy.accessPolicyVersionV0_3()
|
||||
require.NotNil(t, err)
|
||||
require.Equal(t, "model.access_policy.is_valid.session_attribute_on_membership.app_error", err.Id)
|
||||
})
|
||||
|
||||
t.Run("permission rule allows session attribute reference", func(t *testing.T) {
|
||||
policy := &AccessControlPolicy{
|
||||
ID: NewId(),
|
||||
Type: AccessControlPolicyTypePermission,
|
||||
Name: "Permission",
|
||||
Revision: 0,
|
||||
Version: AccessControlPolicyVersionV0_3,
|
||||
Roles: []string{"system_user"},
|
||||
Rules: []AccessControlPolicyRule{{
|
||||
Actions: []string{AccessControlPolicyActionUploadFileAttachment},
|
||||
Expression: "user.session.ip_address == \"10.0.0.1\"",
|
||||
}},
|
||||
}
|
||||
require.Nil(t, policy.accessPolicyVersionV0_3())
|
||||
})
|
||||
|
||||
t.Run("mixed-action rule rejects session attribute reference", func(t *testing.T) {
|
||||
policy := &AccessControlPolicy{
|
||||
ID: NewId(),
|
||||
Type: AccessControlPolicyTypeParent,
|
||||
Name: "Parent",
|
||||
Revision: 0,
|
||||
Version: AccessControlPolicyVersionV0_3,
|
||||
Rules: []AccessControlPolicyRule{{
|
||||
Actions: []string{
|
||||
AccessControlPolicyActionMembership,
|
||||
AccessControlPolicyActionUploadFileAttachment,
|
||||
},
|
||||
Expression: "user.session.ip_address == \"10.0.0.1\"",
|
||||
}},
|
||||
}
|
||||
err := policy.accessPolicyVersionV0_3()
|
||||
require.NotNil(t, err)
|
||||
require.Equal(t, "model.access_policy.is_valid.session_attribute_on_membership.app_error", err.Id)
|
||||
})
|
||||
|
||||
t.Run("membership rule without session reference is accepted", func(t *testing.T) {
|
||||
policy := &AccessControlPolicy{
|
||||
ID: NewId(),
|
||||
Type: AccessControlPolicyTypeParent,
|
||||
Name: "Parent",
|
||||
Revision: 0,
|
||||
Version: AccessControlPolicyVersionV0_3,
|
||||
Rules: []AccessControlPolicyRule{{
|
||||
Actions: []string{AccessControlPolicyActionMembership},
|
||||
Expression: "user.attributes.team == \"eng\"",
|
||||
}},
|
||||
}
|
||||
require.Nil(t, policy.accessPolicyVersionV0_3())
|
||||
})
|
||||
|
||||
t.Run("membership rule rejects user.session inside string literal (lexical check)", func(t *testing.T) {
|
||||
policy := &AccessControlPolicy{
|
||||
ID: NewId(),
|
||||
Type: AccessControlPolicyTypeParent,
|
||||
Name: "Parent",
|
||||
Revision: 0,
|
||||
Version: AccessControlPolicyVersionV0_3,
|
||||
Rules: []AccessControlPolicyRule{{
|
||||
Actions: []string{AccessControlPolicyActionMembership},
|
||||
Expression: "user.attributes.note == \"see user.session for context\"",
|
||||
}},
|
||||
}
|
||||
err := policy.accessPolicyVersionV0_3()
|
||||
require.NotNil(t, err)
|
||||
require.Equal(t, "model.access_policy.is_valid.session_attribute_on_membership.app_error", err.Id)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAccessPolicyVersionV0_4(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ const (
|
|||
ClusterEventInvalidateCacheForPostsUsage ClusterEvent = "inv_posts_usage"
|
||||
ClusterEventInvalidateCacheForTeams ClusterEvent = "inv_teams"
|
||||
ClusterEventInvalidateCacheForContentFlagging ClusterEvent = "inv_content_flagging"
|
||||
ClusterEventInvalidateCacheForSessionAttributes ClusterEvent = "inv_session_attributes"
|
||||
ClusterEventInvalidateCacheForAutoTranslation ClusterEvent = "inv_autotranslation"
|
||||
ClusterEventInvalidateCacheForReadReceipts ClusterEvent = "inv_read_receipts"
|
||||
ClusterEventInvalidateCacheForTemporaryPosts ClusterEvent = "inv_temporary_posts"
|
||||
|
|
|
|||
|
|
@ -140,6 +140,9 @@ type FeatureFlags struct {
|
|||
// ManagedChannelCategories enables server-side managed sidebar category enforcement (Enterprise).
|
||||
ManagedChannelCategories bool
|
||||
|
||||
// Enable collection of request-provided session attributes (user agent, IP address, etc.).
|
||||
SessionAttributes bool
|
||||
|
||||
// FEATURE_FLAG_REMOVAL: DiscoverableChannels - Remove this when the feature is GA.
|
||||
// Gates the per-channel Discoverable toggle and the channel-join-request flow that lets
|
||||
// non-members find a private channel in Browse Channels and request to join it.
|
||||
|
|
@ -205,6 +208,8 @@ func (f *FeatureFlags) SetDefaults() {
|
|||
|
||||
f.ManagedChannelCategories = false
|
||||
|
||||
f.SessionAttributes = false
|
||||
|
||||
f.DiscoverableChannels = false
|
||||
|
||||
f.MobileEphemeralMode = false
|
||||
|
|
|
|||
|
|
@ -35,6 +35,12 @@ const (
|
|||
SessionPropIsGuest = "is_guest"
|
||||
SessionActivityTimeout = 1000 * 60 * 5 // 5 minutes
|
||||
SessionUserAccessTokenExpiryHours = 100 * 365 * 24 // 100 years
|
||||
|
||||
SessionAttributesPropertyFieldUserAgentPlatform = "user_agent_platform"
|
||||
SessionAttributesPropertyFieldUserAgentOS = "user_agent_os"
|
||||
SessionAttributesPropertyFieldUserAgentBrowserName = "user_agent_browser_name"
|
||||
SessionAttributesPropertyFieldUserAgentBrowserVersion = "user_agent_browser_version"
|
||||
SessionAttributesPropertyFieldIPAddress = "ip_address"
|
||||
)
|
||||
|
||||
//msgp:tuple StringMap
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import type {Dispatch} from 'redux';
|
|||
|
||||
import {getAccessControlPolicy as fetchPolicy, createAccessControlPolicy as createPolicy, deleteAccessControlPolicy as deletePolicy} from 'mattermost-redux/actions/access_control';
|
||||
import {getAccessControlSettings, getAccessControlPolicy as getPolicy} from 'mattermost-redux/selectors/entities/access_control';
|
||||
import {getFeatureFlagValue} from 'mattermost-redux/selectors/entities/general';
|
||||
|
||||
import {setNavigationBlocked} from 'actions/admin_actions.jsx';
|
||||
|
||||
|
|
@ -30,6 +31,7 @@ function mapStateToProps(state: GlobalState, ownProps: OwnProps) {
|
|||
policy,
|
||||
policyId,
|
||||
accessControlSettings: config,
|
||||
sessionAttributesEnabled: getFeatureFlagValue(state, 'SessionAttributes') === 'true',
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ export interface PermissionPolicyDetailsProps {
|
|||
policy?: AccessControlPolicy;
|
||||
policyId?: string;
|
||||
accessControlSettings: AccessControlSettings;
|
||||
sessionAttributesEnabled: boolean;
|
||||
actions: PolicyActions;
|
||||
}
|
||||
|
||||
|
|
@ -113,6 +114,7 @@ function PermissionPolicyDetails({
|
|||
policyId,
|
||||
actions,
|
||||
accessControlSettings,
|
||||
sessionAttributesEnabled,
|
||||
}: PermissionPolicyDetailsProps): JSX.Element {
|
||||
const [policyName, setPolicyName] = useState(policy?.name || '');
|
||||
const [expression, setExpression] = useState(policy?.rules?.[0]?.expression || '');
|
||||
|
|
@ -140,7 +142,9 @@ function PermissionPolicyDetails({
|
|||
// the channel-settings Permissions Policy tab.
|
||||
const policySimulationEnabled = useSelector(isPolicySimulationEnabled);
|
||||
|
||||
const noUsableAttributes = attributesLoaded && !hasUsableAttributes(autocompleteResult, accessControlSettings.EnableUserManagedAttributes);
|
||||
// Permission policies can reference session attributes (e.g. user.session.ip_address),
|
||||
// so the editor stays usable even without any configured user attributes when SessionAttributes is on.
|
||||
const noUsableAttributes = attributesLoaded && !sessionAttributesEnabled && !hasUsableAttributes(autocompleteResult, accessControlSettings.EnableUserManagedAttributes);
|
||||
|
||||
useEffect(() => {
|
||||
loadPage().finally(() => setPageLoaded(true));
|
||||
|
|
|
|||
70
webapp/package-lock.json
generated
70
webapp/package-lock.json
generated
|
|
@ -319,7 +319,6 @@
|
|||
"version": "26.1.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cssstyle": "^4.2.1",
|
||||
"data-urls": "^5.0.0",
|
||||
|
|
@ -527,7 +526,6 @@
|
|||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
|
||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.5",
|
||||
|
|
@ -2237,7 +2235,6 @@
|
|||
"integrity": "sha512-DYD23veRYGvBFhcTY1iUvJnDNpuqNd/BzBwCvzOTKUnJjKg5kpUBh3/u9585Agdkgj+QuygG7jLfOPWMa2KVNw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/compat-data": "^7.29.0",
|
||||
"@babel/helper-compilation-targets": "^7.28.6",
|
||||
|
|
@ -2591,7 +2588,6 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
|
|
@ -2615,7 +2611,6 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -2808,7 +2803,6 @@
|
|||
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
|
||||
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.3",
|
||||
"@emotion/babel-plugin": "^11.13.5",
|
||||
|
|
@ -5035,7 +5029,6 @@
|
|||
"integrity": "sha512-LTATglVUPGkPf15zX1wTMlZ0+AU7cGEGF6ekVF1crA8eHUWsGjrYTB+Ht4E3HTrCok8weQG+K01rJndCp/l4XA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.7.2",
|
||||
"@jimp/core": "^0.16.13"
|
||||
|
|
@ -5078,7 +5071,6 @@
|
|||
"integrity": "sha512-8Z1k96ZFxlhK2bgrY1JNWNwvaBeI/bciLM0yDOni2+aZwfIIiC7Y6PeWHTAvjHNjphz+XCt01WQmOYWCn0ML6g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.7.2",
|
||||
"@jimp/utils": "^0.16.13"
|
||||
|
|
@ -5093,7 +5085,6 @@
|
|||
"integrity": "sha512-PvLrfa8vkej3qinlebyhLpksJgCF5aiysDMSVhOZqwH5nQLLtDE9WYbnsofGw4r0VVpyw3H/ANCIzYTyCtP9Cg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.7.2",
|
||||
"@jimp/utils": "^0.16.13"
|
||||
|
|
@ -5122,7 +5113,6 @@
|
|||
"integrity": "sha512-xW+9BtEvoIkkH/Wde9ql4nAFbYLkVINhpgAE7VcBUsuuB34WUbcBl/taOuUYQrPEFQJ4jfXiAJZ2H/rvKjCVnQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.7.2",
|
||||
"@jimp/utils": "^0.16.13",
|
||||
|
|
@ -5172,7 +5162,6 @@
|
|||
"integrity": "sha512-WEl2tPVYwzYL8OKme6Go2xqiWgKsgxlMwyHabdAU4tXaRwOCnOI7v4021gCcBb9zn/oWwguHuKHmK30Fw2Z/PA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.7.2",
|
||||
"@jimp/utils": "^0.16.13"
|
||||
|
|
@ -5316,7 +5305,6 @@
|
|||
"integrity": "sha512-qoqtN8LDknm3fJm9nuPygJv30O3vGhSBD2TxrsCnhtOsxKAqVPJtFVdGd/qVuZ8nqQANQmTlfqTiK9mVWQ7MiQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.7.2",
|
||||
"@jimp/utils": "^0.16.13"
|
||||
|
|
@ -5331,7 +5319,6 @@
|
|||
"integrity": "sha512-Ev+Jjmj1nHYw897z9C3R9dYsPv7S2/nxdgfFb/h8hOwK0Ovd1k/+yYS46A0uj/JCKK0pQk8wOslYBkPwdnLorw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.7.2",
|
||||
"@jimp/utils": "^0.16.13"
|
||||
|
|
@ -5349,7 +5336,6 @@
|
|||
"integrity": "sha512-05POQaEJVucjTiSGMoH68ZiELc7QqpIpuQlZ2JBbhCV+WCbPFUBcGSmE7w4Jd0E2GvCho/NoMODLwgcVGQA97A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.7.2",
|
||||
"@jimp/utils": "^0.16.13"
|
||||
|
|
@ -6092,8 +6078,7 @@
|
|||
"version": "0.1.53",
|
||||
"resolved": "https://registry.npmjs.org/@mattermost/compass-icons/-/compass-icons-0.1.53.tgz",
|
||||
"integrity": "sha512-MPcwJ9bKWxOxppxoqYCK5BW/a9qkaozxQr/fTdu55TLBjV5t0W6EPVa+msnFLr2iStYsYVnHwEo3fArsX0Bnew==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@mattermost/components": {
|
||||
"resolved": "platform/components",
|
||||
|
|
@ -7054,7 +7039,6 @@
|
|||
"integrity": "sha512-a0CgrW5A5kwuSu5J1RFRoMQaMs9yagvfH2jJMYVw56+/7NRI4KOtu612SG9Y1ERWfY55ZwzyFxtLWvD6LO+Anw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@mischnic/json-sourcemap": "^0.1.1",
|
||||
"@parcel/cache": "2.16.4",
|
||||
|
|
@ -9083,7 +9067,6 @@
|
|||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@swc/counter": "^0.1.3",
|
||||
"@swc/types": "^0.1.26"
|
||||
|
|
@ -9335,7 +9318,6 @@
|
|||
"integrity": "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
|
|
@ -9407,7 +9389,6 @@
|
|||
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.10.4",
|
||||
"@babel/runtime": "^7.12.5",
|
||||
|
|
@ -9528,7 +9509,6 @@
|
|||
"integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.20.7",
|
||||
"@babel/types": "^7.20.7",
|
||||
|
|
@ -9972,7 +9952,6 @@
|
|||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.64.tgz",
|
||||
"integrity": "sha512-MlmPvHgjj2p3vZaxbQgFUQFvD8QiZwACfGqEdDSWou5yISWxDQ4/74nCAwsUiX7UFLKZz3BbVSPj+YxeoGGCfg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"@types/scheduler": "*",
|
||||
|
|
@ -10243,7 +10222,6 @@
|
|||
"integrity": "sha512-DqVpl8R0vbhVSop4120UHtGrFmHuPeoDwF4hDT0kPJTY8ty0SI38RV3VhCMsWigMUXG+kCXu7vMRqMFNy6eQgA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/hoist-non-react-statics": "*",
|
||||
"@types/react": "*",
|
||||
|
|
@ -10355,7 +10333,6 @@
|
|||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz",
|
||||
"integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==",
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "7.18.0",
|
||||
"@typescript-eslint/types": "7.18.0",
|
||||
|
|
@ -11048,7 +11025,6 @@
|
|||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
|
|
@ -12100,7 +12076,6 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.10.12",
|
||||
"caniuse-lite": "^1.0.30001782",
|
||||
|
|
@ -12477,8 +12452,7 @@
|
|||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz",
|
||||
"integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/clean-css": {
|
||||
"version": "5.3.3",
|
||||
|
|
@ -12929,6 +12903,7 @@
|
|||
"integrity": "sha512-XM4RFka59xATyJv/cS3O3Kml72hQXUeGRuuTmMYFxwzc9/7C8OYTaIR/Ji+Yt8DXzsFLNhat15cE/JP15HrCgw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/core-js"
|
||||
|
|
@ -13433,7 +13408,6 @@
|
|||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
|
||||
"integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.11"
|
||||
},
|
||||
|
|
@ -14249,7 +14223,6 @@
|
|||
"integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
|
||||
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.6.1",
|
||||
|
|
@ -14882,7 +14855,6 @@
|
|||
"integrity": "sha512-MeVXdReleBTdkz/bvcQMSnCXGi+c9kvy51IpinjnJgutl3YTHWsDdke7Z1ufZpGfDG8xduBDKyjtB9JH1eBKIQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"array-includes": "^3.1.7",
|
||||
"array.prototype.findlast": "^1.2.4",
|
||||
|
|
@ -14916,7 +14888,6 @@
|
|||
"integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
|
|
@ -17838,7 +17809,6 @@
|
|||
"integrity": "sha512-Ry+p2+NLk6u8Agh5yVqELfUJvRfV51hhVBRIB5yZPY7mU0DGBmOuFG5GebZbMbm86cdQNK0fhJuDX8/1YorISQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jest/core": "30.1.3",
|
||||
"@jest/types": "30.0.5",
|
||||
|
|
@ -20424,8 +20394,7 @@
|
|||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
|
||||
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash-es": {
|
||||
"version": "4.18.1",
|
||||
|
|
@ -20952,8 +20921,7 @@
|
|||
"version": "0.52.2",
|
||||
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz",
|
||||
"integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/monaco-editor-webpack-plugin": {
|
||||
"version": "7.1.0",
|
||||
|
|
@ -22122,7 +22090,6 @@
|
|||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
|
@ -22261,7 +22228,6 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
|
|
@ -22436,7 +22402,6 @@
|
|||
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
"util-deprecate": "^1.0.2"
|
||||
|
|
@ -22761,7 +22726,6 @@
|
|||
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
|
||||
"integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
},
|
||||
|
|
@ -22882,7 +22846,6 @@
|
|||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
|
||||
"integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.0"
|
||||
|
|
@ -22962,8 +22925,7 @@
|
|||
"version": "18.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
|
||||
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-is-18": {
|
||||
"name": "react-is",
|
||||
|
|
@ -23369,8 +23331,7 @@
|
|||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/redux-batched-actions": {
|
||||
"version": "0.5.0",
|
||||
|
|
@ -23784,7 +23745,6 @@
|
|||
"integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
|
|
@ -24105,7 +24065,6 @@
|
|||
"integrity": "sha512-TQd2aoQl/+zsxRMEDSxVdpPIqeq9UFc6pr7PzkugiTx3VYCFPUaa3P4RrBQsqok4PO200Vkz0vXQBNlg7W907g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@parcel/watcher": "^2.4.1",
|
||||
"chokidar": "^4.0.0",
|
||||
|
|
@ -24216,7 +24175,6 @@
|
|||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
|
||||
"integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
|
|
@ -24571,8 +24529,7 @@
|
|||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shallow-equals/-/shallow-equals-1.0.0.tgz",
|
||||
"integrity": "sha512-xd/FKcdmfmMbyYCca3QTVEJtqUOGuajNzvAX6nt8dXILwjAIEkfHc4hI8/JMGApAmb7VeULO0Q30NTxnbH/15g==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/shallowequal": {
|
||||
"version": "1.1.0",
|
||||
|
|
@ -25413,7 +25370,6 @@
|
|||
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.7.tgz",
|
||||
"integrity": "sha512-JL1b4A79OGqav4TxkrNsuuQfy6ZnrpyQx6hBDQ3Hd3JyuR2IQuVNBpF+FCEWFNZpN5hj+fhkaEVWteVJ18f0tw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-module-imports": "^7.0.0",
|
||||
"@babel/traverse": "^7.4.5",
|
||||
|
|
@ -25482,7 +25438,6 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@csstools/css-parser-algorithms": "^3.0.1",
|
||||
"@csstools/css-tokenizer": "^3.0.1",
|
||||
|
|
@ -26178,7 +26133,6 @@
|
|||
"resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz",
|
||||
"integrity": "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.9.0"
|
||||
}
|
||||
|
|
@ -26388,8 +26342,7 @@
|
|||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD",
|
||||
"peer": true
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/turndown": {
|
||||
"version": "7.2.2",
|
||||
|
|
@ -26527,7 +26480,6 @@
|
|||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
|
||||
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
|
@ -26962,7 +26914,6 @@
|
|||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.106.2.tgz",
|
||||
"integrity": "sha512-wGN3qcrBQIFmQ/c0AiOAQBvrZ5lmY8vbbMv4Mxfgzqd/B6+9pXtLo73WuS1dSGXM5QYY3hZnIbvx+K1xxe6FyA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/eslint-scope": "^3.7.7",
|
||||
"@types/estree": "^1.0.8",
|
||||
|
|
@ -27011,7 +26962,6 @@
|
|||
"integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@discoveryjs/json-ext": "^0.6.1",
|
||||
"@webpack-cli/configtest": "^3.0.1",
|
||||
|
|
@ -28039,7 +27989,6 @@
|
|||
"version": "26.1.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cssstyle": "^4.2.1",
|
||||
"data-urls": "^5.0.0",
|
||||
|
|
@ -28290,7 +28239,6 @@
|
|||
"version": "26.1.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cssstyle": "^4.2.1",
|
||||
"data-urls": "^5.0.0",
|
||||
|
|
|
|||
|
|
@ -135,6 +135,7 @@ export type ClientConfig = {
|
|||
FeatureFlagContentFlagging: string;
|
||||
FeatureFlagClassificationMarkings: string;
|
||||
FeatureFlagManagedChannelCategories: string;
|
||||
FeatureFlagSessionAttributes: string;
|
||||
|
||||
ForgotPasswordLink: string;
|
||||
GiphySdkKey: string;
|
||||
|
|
|
|||
Loading…
Reference in a new issue