snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
This commit is contained in:
290
server/channels/app/custom_profile_attributes_test.go
Normal file
290
server/channels/app/custom_profile_attributes_test.go
Normal file
@@ -0,0 +1,290 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/request"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetCPAValue(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
|
||||
|
||||
cpaGroup, groupErr := th.App.GetPropertyGroup(request.TestContext(t), model.AccessControlPropertyGroupName)
|
||||
require.Nil(t, groupErr)
|
||||
cpaID := cpaGroup.ID
|
||||
|
||||
rctx := th.emptyContextWithCallerID(anonymousCallerId)
|
||||
|
||||
field := &model.PropertyField{
|
||||
GroupID: cpaID,
|
||||
Name: "f_" + model.NewId(),
|
||||
Type: model.PropertyFieldTypeText,
|
||||
ObjectType: model.PropertyFieldObjectTypeUser,
|
||||
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
||||
}
|
||||
createdField, err := th.App.CreatePropertyField(rctx, field, false, "")
|
||||
require.Nil(t, err)
|
||||
fieldID := createdField.ID
|
||||
|
||||
t.Run("should fail if the value doesn't exist", func(t *testing.T) {
|
||||
pv, appErr := th.App.GetPropertyValue(rctx, cpaID, model.NewId())
|
||||
require.NotNil(t, appErr)
|
||||
require.Nil(t, pv)
|
||||
})
|
||||
|
||||
t.Run("should fail if the group id is invalid", func(t *testing.T) {
|
||||
propertyValue := &model.PropertyValue{
|
||||
TargetID: model.NewId(),
|
||||
TargetType: model.PropertyValueTargetTypeUser,
|
||||
GroupID: model.NewId(),
|
||||
FieldID: fieldID,
|
||||
Value: json.RawMessage(`"Value"`),
|
||||
}
|
||||
created, err := th.App.CreatePropertyValue(rctx, propertyValue)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, created)
|
||||
|
||||
pv, appErr := th.App.GetPropertyValue(rctx, cpaID, created.ID)
|
||||
require.NotNil(t, appErr)
|
||||
require.Nil(t, pv)
|
||||
})
|
||||
|
||||
t.Run("should succeed if id exists", func(t *testing.T) {
|
||||
propertyValue := &model.PropertyValue{
|
||||
TargetID: model.NewId(),
|
||||
TargetType: model.PropertyValueTargetTypeUser,
|
||||
GroupID: cpaID,
|
||||
FieldID: fieldID,
|
||||
Value: json.RawMessage(`"Value"`),
|
||||
}
|
||||
propertyValue, err := th.App.CreatePropertyValue(rctx, propertyValue)
|
||||
require.Nil(t, err)
|
||||
|
||||
pv, appErr := th.App.GetPropertyValue(rctx, cpaID, propertyValue.ID)
|
||||
require.Nil(t, appErr)
|
||||
require.NotNil(t, pv)
|
||||
})
|
||||
|
||||
t.Run("should handle array values correctly", func(t *testing.T) {
|
||||
optionIDs := []string{model.NewId(), model.NewId(), model.NewId()}
|
||||
arrayField := &model.PropertyField{
|
||||
GroupID: cpaID,
|
||||
Name: "f_" + model.NewId(),
|
||||
Type: model.PropertyFieldTypeMultiselect,
|
||||
ObjectType: model.PropertyFieldObjectTypeUser,
|
||||
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
||||
Attrs: model.StringInterface{
|
||||
model.PropertyFieldAttributeOptions: []any{
|
||||
map[string]any{"id": optionIDs[0], "name": "option1"},
|
||||
map[string]any{"id": optionIDs[1], "name": "option2"},
|
||||
map[string]any{"id": optionIDs[2], "name": "option3"},
|
||||
},
|
||||
},
|
||||
}
|
||||
createdField, err := th.App.CreatePropertyField(rctx, arrayField, false, "")
|
||||
require.Nil(t, err)
|
||||
|
||||
propertyValue := &model.PropertyValue{
|
||||
TargetID: model.NewId(),
|
||||
TargetType: model.PropertyValueTargetTypeUser,
|
||||
GroupID: cpaID,
|
||||
FieldID: createdField.ID,
|
||||
Value: json.RawMessage(fmt.Sprintf(`["%s", "%s", "%s"]`, optionIDs[0], optionIDs[1], optionIDs[2])),
|
||||
}
|
||||
propertyValue, err = th.App.CreatePropertyValue(rctx, propertyValue)
|
||||
require.Nil(t, err)
|
||||
|
||||
pv, appErr := th.App.GetPropertyValue(rctx, cpaID, propertyValue.ID)
|
||||
require.Nil(t, appErr)
|
||||
require.NotNil(t, pv)
|
||||
var arrayValues []string
|
||||
require.NoError(t, json.Unmarshal(pv.Value, &arrayValues))
|
||||
require.Equal(t, optionIDs, arrayValues)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeleteCPAValues(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
|
||||
|
||||
cpaGroup, groupErr := th.App.GetPropertyGroup(request.TestContext(t), model.AccessControlPropertyGroupName)
|
||||
require.Nil(t, groupErr)
|
||||
cpaID := cpaGroup.ID
|
||||
|
||||
rctx := th.emptyContextWithCallerID(anonymousCallerId)
|
||||
|
||||
userID := model.NewId()
|
||||
otherUserID := model.NewId()
|
||||
|
||||
listValues := func(targetID string) []*model.PropertyValue {
|
||||
t.Helper()
|
||||
values, appErr := th.App.SearchPropertyValues(rctx, cpaID, model.PropertyValueSearchOpts{
|
||||
TargetIDs: []string{targetID},
|
||||
TargetType: model.PropertyValueTargetTypeUser,
|
||||
// Single-target search: at most one value per (target, field), so the field cap bounds the page.
|
||||
PerPage: model.AccessControlGroupFieldLimit + 5,
|
||||
})
|
||||
require.Nil(t, appErr)
|
||||
return values
|
||||
}
|
||||
|
||||
// Create multiple fields and a value per field for userID.
|
||||
var createdFields []*model.PropertyField
|
||||
for i := 1; i <= 3; i++ {
|
||||
field := &model.PropertyField{
|
||||
GroupID: cpaID,
|
||||
Name: fmt.Sprintf("field_%d", i),
|
||||
Type: model.PropertyFieldTypeText,
|
||||
ObjectType: model.PropertyFieldObjectTypeUser,
|
||||
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
||||
}
|
||||
createdField, err := th.App.CreatePropertyField(rctx, field, false, "")
|
||||
require.Nil(t, err)
|
||||
createdFields = append(createdFields, createdField)
|
||||
|
||||
value := &model.PropertyValue{
|
||||
TargetID: userID,
|
||||
TargetType: model.PropertyValueTargetTypeUser,
|
||||
GroupID: cpaID,
|
||||
FieldID: createdField.ID,
|
||||
Value: json.RawMessage(fmt.Sprintf(`"Value %d"`, i)),
|
||||
}
|
||||
_, err = th.App.CreatePropertyValue(rctx, value)
|
||||
require.Nil(t, err)
|
||||
}
|
||||
|
||||
require.Len(t, listValues(userID), 3)
|
||||
|
||||
t.Run("should delete all values for a user", func(t *testing.T) {
|
||||
appErr := th.App.DeletePropertyValuesForTarget(rctx, cpaID, model.PropertyFieldObjectTypeUser, userID)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
require.Empty(t, listValues(userID))
|
||||
})
|
||||
|
||||
t.Run("should handle deleting values for a user with no values", func(t *testing.T) {
|
||||
appErr := th.App.DeletePropertyValuesForTarget(rctx, cpaID, model.PropertyFieldObjectTypeUser, otherUserID)
|
||||
require.Nil(t, appErr)
|
||||
})
|
||||
|
||||
t.Run("should not affect values for other users", func(t *testing.T) {
|
||||
// Create values for otherUserID.
|
||||
for _, field := range createdFields {
|
||||
value := &model.PropertyValue{
|
||||
TargetID: otherUserID,
|
||||
TargetType: model.PropertyValueTargetTypeUser,
|
||||
GroupID: cpaID,
|
||||
FieldID: field.ID,
|
||||
Value: json.RawMessage(`"Other user value"`),
|
||||
}
|
||||
_, err := th.App.CreatePropertyValue(rctx, value)
|
||||
require.Nil(t, err)
|
||||
}
|
||||
|
||||
appErr := th.App.DeletePropertyValuesForTarget(rctx, cpaID, model.PropertyFieldObjectTypeUser, userID)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
require.Len(t, listValues(otherUserID), 3)
|
||||
})
|
||||
}
|
||||
|
||||
// TestCPAValueSyncLock exercises AccessControlHook.checkSyncLock end-to-end
|
||||
// at the app layer: a write for a field with ldap= or saml= set only
|
||||
// succeeds when the caller ID matches the field's sync source. Covering this
|
||||
// at the app layer also asserts that the startup wiring in server.go
|
||||
// (access_control group registration, AccessControlHook install, and
|
||||
// CallerIDExtractor reading from request.CTX) is intact — something the
|
||||
// properties-package tests cannot verify because they install the hook
|
||||
// themselves.
|
||||
func TestCPAValueSyncLock(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
|
||||
|
||||
cpaGroup, groupErr := th.App.GetPropertyGroup(request.TestContext(t), model.AccessControlPropertyGroupName)
|
||||
require.Nil(t, groupErr)
|
||||
cpaID := cpaGroup.ID
|
||||
|
||||
adminRctx := th.emptyContextWithCallerID(th.SystemAdminUser.Id)
|
||||
|
||||
createField := func(name string, attrs model.CPAAttrs) *model.PropertyField {
|
||||
t.Helper()
|
||||
cpa := &model.CPAField{
|
||||
PropertyField: model.PropertyField{
|
||||
GroupID: cpaID,
|
||||
Name: name,
|
||||
Type: model.PropertyFieldTypeText,
|
||||
ObjectType: model.PropertyFieldObjectTypeUser,
|
||||
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
||||
},
|
||||
Attrs: attrs,
|
||||
}
|
||||
// Sanitization/validation runs inside CreatePropertyField via the
|
||||
// AccessControlAttributeValidationHook.
|
||||
created, appErr := th.App.CreatePropertyField(adminRctx, cpa.ToPropertyField(), false, "")
|
||||
require.Nil(t, appErr)
|
||||
return created
|
||||
}
|
||||
|
||||
ldapField := createField("ldap_attr_"+model.NewId(), model.CPAAttrs{LDAP: "mail"})
|
||||
samlField := createField("saml_attr_"+model.NewId(), model.CPAAttrs{SAML: "email"})
|
||||
plainField := createField("plain_attr_"+model.NewId(), model.CPAAttrs{})
|
||||
|
||||
userID := model.NewId()
|
||||
upsertAs := func(callerID string, field *model.PropertyField) *model.AppError {
|
||||
t.Helper()
|
||||
rctx := th.emptyContextWithCallerID(callerID)
|
||||
_, appErr := th.App.UpsertPropertyValues(rctx, []*model.PropertyValue{{
|
||||
GroupID: cpaID,
|
||||
TargetType: model.PropertyValueTargetTypeUser,
|
||||
TargetID: userID,
|
||||
FieldID: field.ID,
|
||||
Value: json.RawMessage(`"value"`),
|
||||
}}, model.PropertyFieldObjectTypeUser, userID, "")
|
||||
return appErr
|
||||
}
|
||||
|
||||
requireSyncLock := func(appErr *model.AppError) {
|
||||
t.Helper()
|
||||
require.NotNil(t, appErr)
|
||||
require.Equal(t, "app.property.sync_lock.app_error", appErr.Id)
|
||||
}
|
||||
|
||||
t.Run("anonymous caller is blocked on an LDAP-synced field", func(t *testing.T) {
|
||||
requireSyncLock(upsertAs(anonymousCallerId, ldapField))
|
||||
})
|
||||
|
||||
t.Run("anonymous caller is blocked on a SAML-synced field", func(t *testing.T) {
|
||||
requireSyncLock(upsertAs(anonymousCallerId, samlField))
|
||||
})
|
||||
|
||||
t.Run("anonymous caller is allowed on a non-synced field", func(t *testing.T) {
|
||||
require.Nil(t, upsertAs(anonymousCallerId, plainField))
|
||||
})
|
||||
|
||||
t.Run("LDAP sync caller is allowed on an LDAP-synced field", func(t *testing.T) {
|
||||
require.Nil(t, upsertAs(model.CallerIDLDAPSync, ldapField))
|
||||
})
|
||||
|
||||
t.Run("LDAP sync caller is blocked on a SAML-synced field", func(t *testing.T) {
|
||||
requireSyncLock(upsertAs(model.CallerIDLDAPSync, samlField))
|
||||
})
|
||||
|
||||
t.Run("SAML sync caller is allowed on a SAML-synced field", func(t *testing.T) {
|
||||
require.Nil(t, upsertAs(model.CallerIDSAMLSync, samlField))
|
||||
})
|
||||
|
||||
t.Run("SAML sync caller is blocked on an LDAP-synced field", func(t *testing.T) {
|
||||
requireSyncLock(upsertAs(model.CallerIDSAMLSync, ldapField))
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user