mirror of
https://github.com/mattermost/mattermost.git
synced 2026-04-13 04:57:45 -04:00
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-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 mmctl docs (push) Blocked by required conditions
Server CI / Postgres with binary parameters (push) Blocked by required conditions
Server CI / Postgres (push) Blocked by required conditions
Server CI / Postgres (FIPS) (push) Blocked by required conditions
Server CI / Generate Test Coverage (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
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
* Refactor property system with app layer routing and access control separation Establish the app layer as the primary entry point for property operations with intelligent routing based on group type. This architecture separates access-controlled operations (CPA groups) from standard operations, improving performance and code clarity. Architecture Changes: - App layer now routes operations based on group type: - CPA groups -> PropertyAccessService (enforces access control) - Non-CPA groups -> PropertyService (direct, no access control) - PropertyAccessService simplified to handle only CPA operations - Eliminated redundant group type checks throughout the codebase * Move access control routing into PropertyService This change makes the PropertyService the main entrypoint for property related operations, and adds a routing mechanism to decide if extra behaviors or checks should run for each operation, in this case, the property access service logic. To add specific payloads that pluggable checks and operations may need, we use the request context. When the request comes from the API, the endpoints are in charge of adding the caller ID to the payload, and in the case of the plugin API, on receiving a request, the server automatically tags the context with the plugin ID so the property service can react accordingly. Finally, the new design enforces all these checks migrating the actual property logic to internal, non-exposed methods, so any caller from the App layer needs to go through the service checks that decide if pluggable logic is needed, avoiding any possibility of a bypass. * Fix i18n * Fix bad error string * Added nil guards to property methods * Add check for multiple group IDs on value operations * Add nil guard to the plugin checker * Fix build error * Update value tests * Fix linter * Adds early return when content flaggin a thread with no replies * Fix mocks * Clean the state of plugin property tests before each run * Do not wrap appErr on API response and fix i18n * Fix create property field test * Remove the need to cache cpaGroupID as part of the property service * Split the property.go file into multiple * Not found group doesn't bypass access control check * Unexport SetPluginCheckerForTests * Rename plugin context getter to be more PSA specific --------- Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
1275 lines
48 KiB
Go
1275 lines
48 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package api4
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/public/shared/request"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestCreateCPAField(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := SetupConfig(t, func(cfg *model.Config) {
|
|
cfg.FeatureFlags.CustomProfileAttributes = true
|
|
})
|
|
|
|
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
|
field := &model.PropertyField{Name: model.NewId(), Type: model.PropertyFieldTypeText}
|
|
|
|
createdField, resp, err := client.CreateCPAField(context.Background(), field)
|
|
CheckForbiddenStatus(t, resp)
|
|
require.Error(t, err)
|
|
CheckErrorID(t, err, "api.custom_profile_attributes.license_error")
|
|
require.Empty(t, createdField)
|
|
}, "endpoint should not work if no valid license is present")
|
|
|
|
// add a valid license
|
|
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
|
|
|
|
t.Run("a user without admin permissions should not be able to create a field", func(t *testing.T) {
|
|
field := &model.PropertyField{
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
}
|
|
|
|
_, resp, err := th.Client.CreateCPAField(context.Background(), field)
|
|
CheckForbiddenStatus(t, resp)
|
|
require.Error(t, err)
|
|
})
|
|
|
|
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
|
field := &model.PropertyField{Name: model.NewId()}
|
|
|
|
createdField, resp, err := client.CreateCPAField(context.Background(), field)
|
|
CheckBadRequestStatus(t, resp)
|
|
require.Error(t, err)
|
|
require.Empty(t, createdField)
|
|
}, "an invalid field should be rejected")
|
|
|
|
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
|
webSocketClient := th.CreateConnectedWebSocketClient(t)
|
|
|
|
name := model.NewId()
|
|
field := &model.PropertyField{
|
|
Name: fmt.Sprintf(" %s\t", name), // name should be sanitized
|
|
Type: model.PropertyFieldTypeText,
|
|
Attrs: map[string]any{"visibility": "when_set"},
|
|
}
|
|
|
|
createdField, resp, err := client.CreateCPAField(context.Background(), field)
|
|
CheckCreatedStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.NotZero(t, createdField.ID)
|
|
require.Equal(t, name, createdField.Name)
|
|
require.Equal(t, "when_set", createdField.Attrs["visibility"])
|
|
|
|
t.Run("a websocket event should be fired as part of the field creation", func(t *testing.T) {
|
|
var wsField model.PropertyField
|
|
require.Eventually(t, func() bool {
|
|
select {
|
|
case event := <-webSocketClient.EventChannel:
|
|
if event.EventType() == model.WebsocketEventCPAFieldCreated {
|
|
fieldData, err := json.Marshal(event.GetData()["field"])
|
|
require.NoError(t, err)
|
|
require.NoError(t, json.Unmarshal(fieldData, &wsField))
|
|
return true
|
|
}
|
|
default:
|
|
return false
|
|
}
|
|
return false
|
|
}, 5*time.Second, 100*time.Millisecond)
|
|
|
|
require.NotEmpty(t, wsField.ID)
|
|
require.Equal(t, createdField, &wsField)
|
|
})
|
|
}, "a user with admin permissions should be able to create the field")
|
|
|
|
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
|
managedField := &model.PropertyField{
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
Attrs: model.StringInterface{
|
|
model.CustomProfileAttributesPropertyAttrsManaged: "admin",
|
|
"visibility": "when_set",
|
|
},
|
|
}
|
|
|
|
createdManagedField, resp, err := client.CreateCPAField(context.Background(), managedField)
|
|
CheckCreatedStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.NotZero(t, createdManagedField.ID)
|
|
require.Equal(t, managedField.Name, createdManagedField.Name)
|
|
require.Equal(t, "admin", createdManagedField.Attrs[model.CustomProfileAttributesPropertyAttrsManaged])
|
|
require.Equal(t, "when_set", createdManagedField.Attrs["visibility"])
|
|
}, "admin should be able to create a managed field")
|
|
}
|
|
|
|
func TestListCPAFields(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := SetupConfig(t, func(cfg *model.Config) {
|
|
cfg.FeatureFlags.CustomProfileAttributes = true
|
|
})
|
|
|
|
field, err := model.NewCPAFieldFromPropertyField(&model.PropertyField{
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
Attrs: map[string]any{"visibility": "when_set"},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
createdField, appErr := th.App.CreateCPAField(request.TestContext(t), field)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdField)
|
|
|
|
t.Run("endpoint should not work if no valid license is present", func(t *testing.T) {
|
|
fields, resp, err := th.Client.ListCPAFields(context.Background())
|
|
CheckForbiddenStatus(t, resp)
|
|
require.Error(t, err)
|
|
CheckErrorID(t, err, "api.custom_profile_attributes.license_error")
|
|
require.Empty(t, fields)
|
|
})
|
|
|
|
// add a valid license
|
|
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
|
|
|
|
t.Run("any user should be able to list fields", func(t *testing.T) {
|
|
fields, resp, err := th.Client.ListCPAFields(context.Background())
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, fields)
|
|
require.Len(t, fields, 1)
|
|
require.Equal(t, createdField.ID, fields[0].ID)
|
|
})
|
|
|
|
t.Run("the endpoint should only list non deleted fields", func(t *testing.T) {
|
|
require.Nil(t, th.App.DeleteCPAField(request.TestContext(t), createdField.ID))
|
|
fields, resp, err := th.Client.ListCPAFields(context.Background())
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.Empty(t, fields)
|
|
})
|
|
}
|
|
|
|
func TestPatchCPAField(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := SetupConfig(t, func(cfg *model.Config) {
|
|
cfg.FeatureFlags.CustomProfileAttributes = true
|
|
})
|
|
|
|
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
|
patch := &model.PropertyFieldPatch{Name: model.NewPointer(model.NewId())}
|
|
patchedField, resp, err := client.PatchCPAField(context.Background(), model.NewId(), patch)
|
|
CheckForbiddenStatus(t, resp)
|
|
require.Error(t, err)
|
|
CheckErrorID(t, err, "api.custom_profile_attributes.license_error")
|
|
require.Empty(t, patchedField)
|
|
}, "endpoint should not work if no valid license is present")
|
|
|
|
// add a valid license
|
|
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
|
|
|
|
t.Run("a user without admin permissions should not be able to patch a field", func(t *testing.T) {
|
|
field, err := model.NewCPAFieldFromPropertyField(&model.PropertyField{
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
createdField, appErr := th.App.CreateCPAField(request.TestContext(t), field)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdField)
|
|
|
|
patch := &model.PropertyFieldPatch{Name: model.NewPointer(model.NewId())}
|
|
_, resp, err := th.Client.PatchCPAField(context.Background(), createdField.ID, patch)
|
|
CheckForbiddenStatus(t, resp)
|
|
require.Error(t, err)
|
|
})
|
|
|
|
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
|
webSocketClient := th.CreateConnectedWebSocketClient(t)
|
|
|
|
field, err := model.NewCPAFieldFromPropertyField(&model.PropertyField{
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
createdField, appErr := th.App.CreateCPAField(request.TestContext(t), field)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdField)
|
|
|
|
newName := model.NewId()
|
|
patch := &model.PropertyFieldPatch{Name: model.NewPointer(fmt.Sprintf(" %s \t ", newName))} // name should be sanitized
|
|
patchedField, resp, err := client.PatchCPAField(context.Background(), createdField.ID, patch)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.Equal(t, newName, patchedField.Name)
|
|
|
|
t.Run("a websocket event should be fired as part of the field patch", func(t *testing.T) {
|
|
var wsField model.PropertyField
|
|
require.Eventually(t, func() bool {
|
|
select {
|
|
case event := <-webSocketClient.EventChannel:
|
|
if event.EventType() == model.WebsocketEventCPAFieldUpdated {
|
|
fieldData, err := json.Marshal(event.GetData()["field"])
|
|
require.NoError(t, err)
|
|
require.NoError(t, json.Unmarshal(fieldData, &wsField))
|
|
return true
|
|
}
|
|
default:
|
|
return false
|
|
}
|
|
return false
|
|
}, 5*time.Second, 100*time.Millisecond)
|
|
|
|
require.NotEmpty(t, wsField.ID)
|
|
require.Equal(t, patchedField, &wsField)
|
|
})
|
|
|
|
t.Run("sanitization should remove options and sync details when necessary", func(t *testing.T) {
|
|
// Create a select field with options
|
|
optionID1 := model.NewId()
|
|
optionID2 := model.NewId()
|
|
selectField, err := model.NewCPAFieldFromPropertyField(&model.PropertyField{
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeSelect,
|
|
Attrs: model.StringInterface{
|
|
"options": []map[string]any{
|
|
{"id": optionID1, "name": "Option 1", "color": "#FF0000"},
|
|
{"id": optionID2, "name": "Option 2", "color": "#00FF00"},
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
createdField, _, err := client.CreateCPAField(context.Background(), selectField.ToPropertyField())
|
|
require.NoError(t, err)
|
|
require.NotNil(t, createdField)
|
|
|
|
// Verify options were created
|
|
options, ok := createdField.Attrs["options"]
|
|
require.True(t, ok)
|
|
require.NotNil(t, options)
|
|
|
|
// Patch to change type to text with LDAP attribute
|
|
// Options should be automatically removed even though we don't explicitly remove them
|
|
ldapAttr := "user_attribute"
|
|
textPatch := &model.PropertyFieldPatch{
|
|
Type: model.NewPointer(model.PropertyFieldTypeText),
|
|
Attrs: &model.StringInterface{"ldap": ldapAttr},
|
|
}
|
|
|
|
patchedTextField, resp, err := client.PatchCPAField(context.Background(), createdField.ID, textPatch)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.Equal(t, model.PropertyFieldTypeText, patchedTextField.Type)
|
|
|
|
// Verify options were removed
|
|
options = patchedTextField.Attrs["options"]
|
|
require.Empty(t, options)
|
|
|
|
// Verify LDAP attribute was set
|
|
ldap, ok := patchedTextField.Attrs["ldap"]
|
|
require.True(t, ok)
|
|
require.Equal(t, ldapAttr, ldap)
|
|
|
|
// Now patch to change type to date
|
|
// LDAP attribute should be automatically removed even though we don't explicitly remove it
|
|
datePatch := &model.PropertyFieldPatch{
|
|
Type: model.NewPointer(model.PropertyFieldTypeDate),
|
|
}
|
|
|
|
patchedDateField, resp, err := client.PatchCPAField(context.Background(), patchedTextField.ID, datePatch)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.Equal(t, model.PropertyFieldTypeDate, patchedDateField.Type)
|
|
|
|
// Verify LDAP attribute was removed
|
|
ldap = patchedDateField.Attrs["ldap"]
|
|
require.Empty(t, ldap)
|
|
})
|
|
}, "a user with admin permissions should be able to patch the field")
|
|
|
|
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
|
// Create a regular field first
|
|
field, err := model.NewCPAFieldFromPropertyField(&model.PropertyField{
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
createdField, appErr := th.App.CreateCPAField(request.TestContext(t), field)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdField)
|
|
|
|
// Verify field is not isManaged initially
|
|
require.Empty(t, createdField.Attrs.Managed)
|
|
|
|
// Patch to make it managed
|
|
managedPatch := &model.PropertyFieldPatch{
|
|
Attrs: &model.StringInterface{
|
|
model.CustomProfileAttributesPropertyAttrsManaged: "admin",
|
|
},
|
|
}
|
|
|
|
patchedManagedField, resp, err := client.PatchCPAField(context.Background(), createdField.ID, managedPatch)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "admin", patchedManagedField.Attrs[model.CustomProfileAttributesPropertyAttrsManaged])
|
|
|
|
// Patch to remove managed attribute
|
|
unManagedPatch := &model.PropertyFieldPatch{
|
|
Attrs: &model.StringInterface{
|
|
model.CustomProfileAttributesPropertyAttrsManaged: "",
|
|
},
|
|
}
|
|
|
|
patchedUnmanagedField, resp, err := client.PatchCPAField(context.Background(), patchedManagedField.ID, unManagedPatch)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
|
|
// Verify managed attribute is removed or empty
|
|
require.Empty(t, patchedUnmanagedField.Attrs[model.CustomProfileAttributesPropertyAttrsManaged])
|
|
}, "admin should be able to toggle managed attribute on existing field")
|
|
}
|
|
|
|
func TestDeleteCPAField(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := SetupConfig(t, func(cfg *model.Config) {
|
|
cfg.FeatureFlags.CustomProfileAttributes = true
|
|
})
|
|
|
|
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
|
resp, err := client.DeleteCPAField(context.Background(), model.NewId())
|
|
CheckForbiddenStatus(t, resp)
|
|
require.Error(t, err)
|
|
CheckErrorID(t, err, "api.custom_profile_attributes.license_error")
|
|
}, "endpoint should not work if no valid license is present")
|
|
|
|
// add a valid license
|
|
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
|
|
|
|
t.Run("a user without admin permissions should not be able to delete a field", func(t *testing.T) {
|
|
field := &model.PropertyField{
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
}
|
|
createdField, _, err := th.SystemAdminClient.CreateCPAField(context.Background(), field)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, createdField)
|
|
|
|
resp, err := th.Client.DeleteCPAField(context.Background(), createdField.ID)
|
|
CheckForbiddenStatus(t, resp)
|
|
require.Error(t, err)
|
|
})
|
|
|
|
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
|
webSocketClient := th.CreateConnectedWebSocketClient(t)
|
|
|
|
field := &model.PropertyField{
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
}
|
|
createdField, _, err := th.SystemAdminClient.CreateCPAField(context.Background(), field)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, createdField)
|
|
require.Zero(t, createdField.DeleteAt)
|
|
|
|
resp, err := client.DeleteCPAField(context.Background(), createdField.ID)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
|
|
deletedField, appErr := th.App.GetCPAField(request.TestContext(t), createdField.ID)
|
|
require.Nil(t, appErr)
|
|
require.NotZero(t, deletedField.DeleteAt)
|
|
|
|
t.Run("a websocket event should be fired as part of the field deletion", func(t *testing.T) {
|
|
var fieldID string
|
|
require.Eventually(t, func() bool {
|
|
select {
|
|
case event := <-webSocketClient.EventChannel:
|
|
if event.EventType() == model.WebsocketEventCPAFieldDeleted {
|
|
var ok bool
|
|
fieldID, ok = event.GetData()["field_id"].(string)
|
|
require.True(t, ok)
|
|
return true
|
|
}
|
|
default:
|
|
return false
|
|
}
|
|
return false
|
|
}, 5*time.Second, 100*time.Millisecond)
|
|
|
|
require.Equal(t, createdField.ID, fieldID)
|
|
})
|
|
}, "a user with admin permissions should be able to delete the field")
|
|
}
|
|
|
|
func TestListCPAValues(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
|
|
th := SetupConfig(t, func(cfg *model.Config) {
|
|
cfg.FeatureFlags.CustomProfileAttributes = true
|
|
}).InitBasic(t)
|
|
|
|
th.RemovePermissionFromRole(t, model.PermissionViewMembers.Id, model.SystemUserRoleId)
|
|
defer th.AddPermissionToRole(t, model.PermissionViewMembers.Id, model.SystemUserRoleId)
|
|
|
|
field, err := model.NewCPAFieldFromPropertyField(&model.PropertyField{
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
createdField, appErr := th.App.CreateCPAField(request.TestContext(t), field)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdField)
|
|
|
|
_, appErr = th.App.PatchCPAValue(request.TestContext(t), th.BasicUser.Id, createdField.ID, json.RawMessage(`"Field Value"`), true)
|
|
require.Nil(t, appErr)
|
|
|
|
t.Run("endpoint should not work if no valid license is present", func(t *testing.T) {
|
|
values, resp, err := th.Client.ListCPAValues(context.Background(), th.BasicUser.Id)
|
|
CheckForbiddenStatus(t, resp)
|
|
require.Error(t, err)
|
|
CheckErrorID(t, err, "api.custom_profile_attributes.license_error")
|
|
require.Empty(t, values)
|
|
})
|
|
|
|
// add a valid license
|
|
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
|
|
|
|
// login with Client2 from this point on
|
|
th.LoginBasic2(t)
|
|
|
|
t.Run("any team member should be able to list values", func(t *testing.T) {
|
|
values, resp, err := th.Client.ListCPAValues(context.Background(), th.BasicUser.Id)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, values)
|
|
require.Len(t, values, 1)
|
|
})
|
|
|
|
t.Run("should handle array values correctly", func(t *testing.T) {
|
|
optionID1 := model.NewId()
|
|
optionID2 := model.NewId()
|
|
arrayField, err := model.NewCPAFieldFromPropertyField(&model.PropertyField{
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeMultiselect,
|
|
Attrs: model.StringInterface{
|
|
"options": []map[string]any{
|
|
{"id": optionID1, "name": "option1"},
|
|
{"id": optionID2, "name": "option2"},
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
createdArrayField, appErr := th.App.CreateCPAField(request.TestContext(t), arrayField)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdArrayField)
|
|
|
|
_, appErr = th.App.PatchCPAValue(request.TestContext(t), th.BasicUser.Id, createdArrayField.ID, json.RawMessage(fmt.Sprintf(`["%s", "%s"]`, optionID1, optionID2)), true)
|
|
require.Nil(t, appErr)
|
|
|
|
values, resp, err := th.Client.ListCPAValues(context.Background(), th.BasicUser.Id)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, values)
|
|
|
|
var arrayValues []string
|
|
require.NoError(t, json.Unmarshal(values[createdArrayField.ID], &arrayValues))
|
|
require.ElementsMatch(t, []string{optionID1, optionID2}, arrayValues)
|
|
})
|
|
|
|
t.Run("non team member should NOT be able to list values", func(t *testing.T) {
|
|
resp, err := th.SystemAdminClient.RemoveTeamMember(context.Background(), th.BasicTeam.Id, th.BasicUser2.Id)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
|
|
_, resp, err = th.Client.ListCPAValues(context.Background(), th.BasicUser.Id)
|
|
CheckForbiddenStatus(t, resp)
|
|
require.Error(t, err)
|
|
})
|
|
}
|
|
|
|
func TestPatchCPAValues(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
|
|
th := SetupConfig(t, func(cfg *model.Config) {
|
|
cfg.FeatureFlags.CustomProfileAttributes = true
|
|
}).InitBasic(t)
|
|
|
|
field, err := model.NewCPAFieldFromPropertyField(&model.PropertyField{
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
createdField, appErr := th.App.CreateCPAField(request.TestContext(t), field)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdField)
|
|
|
|
t.Run("endpoint should not work if no valid license is present", func(t *testing.T) {
|
|
values := map[string]json.RawMessage{createdField.ID: json.RawMessage(`"Field Value"`)}
|
|
patchedValues, resp, err := th.Client.PatchCPAValues(context.Background(), values)
|
|
CheckForbiddenStatus(t, resp)
|
|
require.Error(t, err)
|
|
CheckErrorID(t, err, "api.custom_profile_attributes.license_error")
|
|
require.Empty(t, patchedValues)
|
|
})
|
|
|
|
// add a valid license
|
|
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
|
|
|
|
t.Run("any team member should be able to create their own values", func(t *testing.T) {
|
|
webSocketClient := th.CreateConnectedWebSocketClient(t)
|
|
|
|
values := map[string]json.RawMessage{}
|
|
value := "Field Value"
|
|
values[createdField.ID] = json.RawMessage(fmt.Sprintf(`" %s "`, value)) // value should be sanitized
|
|
patchedValues, resp, err := th.Client.PatchCPAValues(context.Background(), values)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, patchedValues)
|
|
require.Len(t, patchedValues, 1)
|
|
var actualValue string
|
|
require.NoError(t, json.Unmarshal(patchedValues[createdField.ID], &actualValue))
|
|
require.Equal(t, value, actualValue)
|
|
|
|
values, resp, err = th.Client.ListCPAValues(context.Background(), th.BasicUser.Id)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, values)
|
|
require.Len(t, values, 1)
|
|
actualValue = ""
|
|
require.NoError(t, json.Unmarshal(values[createdField.ID], &actualValue))
|
|
require.Equal(t, value, actualValue)
|
|
|
|
t.Run("a websocket event should be fired as part of the value changes", func(t *testing.T) {
|
|
var wsValues map[string]json.RawMessage
|
|
require.Eventually(t, func() bool {
|
|
select {
|
|
case event := <-webSocketClient.EventChannel:
|
|
if event.EventType() == model.WebsocketEventCPAValuesUpdated {
|
|
valuesData, err := json.Marshal(event.GetData()["values"])
|
|
require.NoError(t, err)
|
|
require.NoError(t, json.Unmarshal(valuesData, &wsValues))
|
|
return true
|
|
}
|
|
default:
|
|
return false
|
|
}
|
|
return false
|
|
}, 5*time.Second, 100*time.Millisecond)
|
|
|
|
require.NotEmpty(t, wsValues)
|
|
require.Equal(t, patchedValues, wsValues)
|
|
})
|
|
})
|
|
|
|
t.Run("any team member should be able to patch their own values", func(t *testing.T) {
|
|
values, resp, err := th.Client.ListCPAValues(context.Background(), th.BasicUser.Id)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, values)
|
|
require.Len(t, values, 1)
|
|
|
|
value := "Updated Field Value"
|
|
values[createdField.ID] = json.RawMessage(fmt.Sprintf(`" %s \t"`, value)) // value should be sanitized
|
|
patchedValues, resp, err := th.Client.PatchCPAValues(context.Background(), values)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
var actualValue string
|
|
require.NoError(t, json.Unmarshal(patchedValues[createdField.ID], &actualValue))
|
|
require.Equal(t, value, actualValue)
|
|
|
|
values, resp, err = th.Client.ListCPAValues(context.Background(), th.BasicUser.Id)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
actualValue = ""
|
|
require.NoError(t, json.Unmarshal(values[createdField.ID], &actualValue))
|
|
require.Equal(t, value, actualValue)
|
|
})
|
|
|
|
t.Run("should handle array values correctly", func(t *testing.T) {
|
|
optionsID := []string{model.NewId(), model.NewId(), model.NewId(), model.NewId()}
|
|
|
|
arrayField, err := model.NewCPAFieldFromPropertyField(&model.PropertyField{
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeMultiselect,
|
|
Attrs: model.StringInterface{
|
|
"options": []map[string]any{
|
|
{"id": optionsID[0], "name": "option1"},
|
|
{"id": optionsID[1], "name": "option2"},
|
|
{"id": optionsID[2], "name": "option3"},
|
|
{"id": optionsID[3], "name": "option4"},
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
createdArrayField, appErr := th.App.CreateCPAField(request.TestContext(t), arrayField)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdArrayField)
|
|
|
|
values := map[string]json.RawMessage{
|
|
createdArrayField.ID: json.RawMessage(fmt.Sprintf(`["%s", "%s", "%s"]`, optionsID[0], optionsID[1], optionsID[2])),
|
|
}
|
|
patchedValues, resp, err := th.Client.PatchCPAValues(context.Background(), values)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, patchedValues)
|
|
|
|
var actualValues []string
|
|
require.NoError(t, json.Unmarshal(patchedValues[createdArrayField.ID], &actualValues))
|
|
require.Equal(t, optionsID[:3], actualValues)
|
|
|
|
// Test updating array values
|
|
values[createdArrayField.ID] = json.RawMessage(fmt.Sprintf(`["%s", "%s"]`, optionsID[2], optionsID[3]))
|
|
patchedValues, resp, err = th.Client.PatchCPAValues(context.Background(), values)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
|
|
actualValues = nil
|
|
require.NoError(t, json.Unmarshal(patchedValues[createdArrayField.ID], &actualValues))
|
|
require.Equal(t, optionsID[2:4], actualValues)
|
|
})
|
|
|
|
t.Run("should fail if any of the values belongs to a field that is LDAP/SAML synced", func(t *testing.T) {
|
|
// Create a field with LDAP attribute
|
|
ldapField, err := model.NewCPAFieldFromPropertyField(&model.PropertyField{
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
Attrs: model.StringInterface{
|
|
model.CustomProfileAttributesPropertyAttrsLDAP: "ldap_attr",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
createdLDAPField, appErr := th.App.CreateCPAField(request.TestContext(t), ldapField)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdLDAPField)
|
|
|
|
// Create a field with SAML attribute
|
|
samlField, err := model.NewCPAFieldFromPropertyField(&model.PropertyField{
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
Attrs: model.StringInterface{
|
|
model.CustomProfileAttributesPropertyAttrsSAML: "saml_attr",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
createdSAMLField, appErr := th.App.CreateCPAField(request.TestContext(t), samlField)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdSAMLField)
|
|
|
|
// Test LDAP field
|
|
values := map[string]json.RawMessage{
|
|
createdLDAPField.ID: json.RawMessage(`"LDAP Value"`),
|
|
}
|
|
_, resp, err := th.Client.PatchCPAValues(context.Background(), values)
|
|
CheckBadRequestStatus(t, resp)
|
|
require.Error(t, err)
|
|
CheckErrorID(t, err, "app.custom_profile_attributes.property_field_is_synced.app_error")
|
|
|
|
// Test SAML field
|
|
values = map[string]json.RawMessage{
|
|
createdSAMLField.ID: json.RawMessage(`"SAML Value"`),
|
|
}
|
|
_, resp, err = th.Client.PatchCPAValues(context.Background(), values)
|
|
CheckBadRequestStatus(t, resp)
|
|
require.Error(t, err)
|
|
CheckErrorID(t, err, "app.custom_profile_attributes.property_field_is_synced.app_error")
|
|
|
|
// Test multiple fields with one being LDAP synced
|
|
values = map[string]json.RawMessage{
|
|
createdField.ID: json.RawMessage(`"Regular Value"`),
|
|
createdLDAPField.ID: json.RawMessage(`"LDAP Value"`),
|
|
}
|
|
_, resp, err = th.Client.PatchCPAValues(context.Background(), values)
|
|
CheckBadRequestStatus(t, resp)
|
|
require.Error(t, err)
|
|
CheckErrorID(t, err, "app.custom_profile_attributes.property_field_is_synced.app_error")
|
|
})
|
|
|
|
t.Run("an invalid patch should be rejected", func(t *testing.T) {
|
|
field, err := model.NewCPAFieldFromPropertyField(&model.PropertyField{
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
createdField, appErr := th.App.CreateCPAField(request.TestContext(t), field)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdField)
|
|
|
|
// Create a value that's too long (over 64 characters)
|
|
tooLongValue := strings.Repeat("a", model.CPAValueTypeTextMaxLength+1)
|
|
values := map[string]json.RawMessage{
|
|
createdField.ID: json.RawMessage(fmt.Sprintf(`"%s"`, tooLongValue)),
|
|
}
|
|
|
|
_, resp, err := th.Client.PatchCPAValues(context.Background(), values)
|
|
CheckBadRequestStatus(t, resp)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "Failed to validate property value")
|
|
})
|
|
|
|
t.Run("admin-managed fields", func(t *testing.T) {
|
|
// Create a managed field (only admins can create fields)
|
|
managedField := &model.PropertyField{
|
|
Name: "Managed Field",
|
|
Type: model.PropertyFieldTypeText,
|
|
Attrs: model.StringInterface{
|
|
model.CustomProfileAttributesPropertyAttrsManaged: "admin",
|
|
},
|
|
}
|
|
|
|
createdManagedField, resp, err := th.SystemAdminClient.CreateCPAField(context.Background(), managedField)
|
|
CheckCreatedStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, createdManagedField)
|
|
|
|
// Create a non-managed field for comparison
|
|
regularField := &model.PropertyField{
|
|
Name: "Regular Field",
|
|
Type: model.PropertyFieldTypeText,
|
|
}
|
|
|
|
createdRegularField, resp, err := th.SystemAdminClient.CreateCPAField(context.Background(), regularField)
|
|
CheckCreatedStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, createdRegularField)
|
|
|
|
t.Run("regular user cannot update managed field", func(t *testing.T) {
|
|
values := map[string]json.RawMessage{
|
|
createdManagedField.ID: json.RawMessage(`"Managed Value"`),
|
|
}
|
|
|
|
_, resp, err := th.Client.PatchCPAValues(context.Background(), values)
|
|
CheckForbiddenStatus(t, resp)
|
|
require.Error(t, err)
|
|
CheckErrorID(t, err, "app.custom_profile_attributes.property_field_is_managed.app_error")
|
|
})
|
|
|
|
t.Run("regular user can update non-managed field", func(t *testing.T) {
|
|
values := map[string]json.RawMessage{
|
|
createdRegularField.ID: json.RawMessage(`"Regular Value"`),
|
|
}
|
|
|
|
patchedValues, resp, err := th.Client.PatchCPAValues(context.Background(), values)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, patchedValues)
|
|
|
|
var actualValue string
|
|
require.NoError(t, json.Unmarshal(patchedValues[createdRegularField.ID], &actualValue))
|
|
require.Equal(t, "Regular Value", actualValue)
|
|
})
|
|
|
|
t.Run("system admin can update managed field", func(t *testing.T) {
|
|
values := map[string]json.RawMessage{
|
|
createdManagedField.ID: json.RawMessage(`"Admin Updated Value"`),
|
|
}
|
|
|
|
patchedValues, resp, err := th.SystemAdminClient.PatchCPAValues(context.Background(), values)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, patchedValues)
|
|
|
|
var actualValue string
|
|
require.NoError(t, json.Unmarshal(patchedValues[createdManagedField.ID], &actualValue))
|
|
require.Equal(t, "Admin Updated Value", actualValue)
|
|
})
|
|
|
|
t.Run("batch update with managed fields fails for regular user", func(t *testing.T) {
|
|
// First set some initial values to ensure we can verify they don't change
|
|
// Set initial values for both fields using th.App (admins can set managed field values)
|
|
_, appErr := th.App.PatchCPAValue(request.TestContext(t), th.BasicUser.Id, createdRegularField.ID, json.RawMessage(`"Initial Regular Value"`), false)
|
|
require.Nil(t, appErr)
|
|
|
|
_, appErr = th.App.PatchCPAValue(request.TestContext(t), th.BasicUser.Id, createdManagedField.ID, json.RawMessage(`"Initial Managed Value"`), true)
|
|
require.Nil(t, appErr)
|
|
|
|
// Try to batch update both managed and regular fields - this should fail
|
|
attemptedValues := map[string]json.RawMessage{
|
|
createdManagedField.ID: json.RawMessage(`"Managed Batch Value"`),
|
|
createdRegularField.ID: json.RawMessage(`"Regular Batch Value"`),
|
|
}
|
|
|
|
_, resp, err := th.Client.PatchCPAValues(context.Background(), attemptedValues)
|
|
CheckForbiddenStatus(t, resp)
|
|
require.Error(t, err)
|
|
CheckErrorID(t, err, "app.custom_profile_attributes.property_field_is_managed.app_error")
|
|
|
|
// Verify that no values were updated when the batch operation failed
|
|
currentValues, appErr := th.App.ListCPAValues(request.TestContext(t), th.BasicUser.Id)
|
|
require.Nil(t, appErr)
|
|
|
|
// Check that values remain unchanged - both fields should retain their initial values
|
|
regularFieldHasOriginalValue := false
|
|
managedFieldHasOriginalValue := false
|
|
|
|
for _, value := range currentValues {
|
|
if value.FieldID == createdManagedField.ID {
|
|
var currentValue string
|
|
require.NoError(t, json.Unmarshal(value.Value, ¤tValue))
|
|
if currentValue == "Initial Managed Value" {
|
|
managedFieldHasOriginalValue = true
|
|
}
|
|
// Verify it's not the attempted update value
|
|
require.NotEqual(t, "Managed Batch Value", currentValue, "Managed field should not have been updated in failed batch operation")
|
|
}
|
|
if value.FieldID == createdRegularField.ID {
|
|
var currentValue string
|
|
require.NoError(t, json.Unmarshal(value.Value, ¤tValue))
|
|
if currentValue == "Initial Regular Value" {
|
|
regularFieldHasOriginalValue = true
|
|
}
|
|
// Verify it's not the attempted update value
|
|
require.NotEqual(t, "Regular Batch Value", currentValue, "Regular field should not have been updated in failed batch operation")
|
|
}
|
|
}
|
|
|
|
// Both fields should retain their original values after the failed batch operation
|
|
require.True(t, regularFieldHasOriginalValue, "Regular field should retain its original value")
|
|
require.True(t, managedFieldHasOriginalValue, "Managed field should retain its original value")
|
|
})
|
|
|
|
t.Run("batch update with managed fields succeeds for admin", func(t *testing.T) {
|
|
values := map[string]json.RawMessage{
|
|
createdManagedField.ID: json.RawMessage(`"Admin Managed Batch"`),
|
|
createdRegularField.ID: json.RawMessage(`"Admin Regular Batch"`),
|
|
}
|
|
|
|
patchedValues, resp, err := th.SystemAdminClient.PatchCPAValues(context.Background(), values)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.Len(t, patchedValues, 2)
|
|
|
|
var managedValue, regularValue string
|
|
require.NoError(t, json.Unmarshal(patchedValues[createdManagedField.ID], &managedValue))
|
|
require.NoError(t, json.Unmarshal(patchedValues[createdRegularField.ID], ®ularValue))
|
|
require.Equal(t, "Admin Managed Batch", managedValue)
|
|
require.Equal(t, "Admin Regular Batch", regularValue)
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestPatchCPAValuesForUser(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
|
|
th := SetupConfig(t, func(cfg *model.Config) {
|
|
cfg.FeatureFlags.CustomProfileAttributes = true
|
|
}).InitBasic(t)
|
|
|
|
field, err := model.NewCPAFieldFromPropertyField(&model.PropertyField{
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
createdField, appErr := th.App.CreateCPAField(request.TestContext(t), field)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdField)
|
|
|
|
t.Run("endpoint should not work if no valid license is present", func(t *testing.T) {
|
|
values := map[string]json.RawMessage{createdField.ID: json.RawMessage(`"Field Value"`)}
|
|
patchedValues, resp, err := th.Client.PatchCPAValuesForUser(context.Background(), th.BasicUser.Id, values)
|
|
CheckForbiddenStatus(t, resp)
|
|
require.Error(t, err)
|
|
CheckErrorID(t, err, "api.custom_profile_attributes.license_error")
|
|
require.Empty(t, patchedValues)
|
|
})
|
|
|
|
// add a valid license
|
|
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
|
|
|
|
t.Run("any team member should be able to create their own values", func(t *testing.T) {
|
|
webSocketClient := th.CreateConnectedWebSocketClient(t)
|
|
|
|
values := map[string]json.RawMessage{}
|
|
value := "Field Value"
|
|
values[createdField.ID] = json.RawMessage(fmt.Sprintf(`" %s "`, value)) // value should be sanitized
|
|
patchedValues, resp, err := th.Client.PatchCPAValuesForUser(context.Background(), th.BasicUser.Id, values)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, patchedValues)
|
|
require.Len(t, patchedValues, 1)
|
|
var actualValue string
|
|
require.NoError(t, json.Unmarshal(patchedValues[createdField.ID], &actualValue))
|
|
require.Equal(t, value, actualValue)
|
|
|
|
values, resp, err = th.Client.ListCPAValues(context.Background(), th.BasicUser.Id)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, values)
|
|
require.Len(t, values, 1)
|
|
actualValue = ""
|
|
require.NoError(t, json.Unmarshal(values[createdField.ID], &actualValue))
|
|
require.Equal(t, value, actualValue)
|
|
|
|
t.Run("a websocket event should be fired as part of the value changes", func(t *testing.T) {
|
|
var wsValues map[string]json.RawMessage
|
|
require.Eventually(t, func() bool {
|
|
select {
|
|
case event := <-webSocketClient.EventChannel:
|
|
if event.EventType() == model.WebsocketEventCPAValuesUpdated {
|
|
valuesData, err := json.Marshal(event.GetData()["values"])
|
|
require.NoError(t, err)
|
|
require.NoError(t, json.Unmarshal(valuesData, &wsValues))
|
|
return true
|
|
}
|
|
default:
|
|
return false
|
|
}
|
|
return false
|
|
}, 5*time.Second, 100*time.Millisecond)
|
|
|
|
require.NotEmpty(t, wsValues)
|
|
require.Equal(t, patchedValues, wsValues)
|
|
})
|
|
})
|
|
|
|
t.Run("any team member should be able to patch their own values", func(t *testing.T) {
|
|
values, resp, err := th.Client.ListCPAValues(context.Background(), th.BasicUser.Id)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, values)
|
|
require.Len(t, values, 1)
|
|
|
|
value := "Updated Field Value"
|
|
values[createdField.ID] = json.RawMessage(fmt.Sprintf(`" %s \t"`, value)) // value should be sanitized
|
|
patchedValues, resp, err := th.Client.PatchCPAValuesForUser(context.Background(), th.BasicUser.Id, values)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
var actualValue string
|
|
require.NoError(t, json.Unmarshal(patchedValues[createdField.ID], &actualValue))
|
|
require.Equal(t, value, actualValue)
|
|
|
|
values, resp, err = th.Client.ListCPAValues(context.Background(), th.BasicUser.Id)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
actualValue = ""
|
|
require.NoError(t, json.Unmarshal(values[createdField.ID], &actualValue))
|
|
require.Equal(t, value, actualValue)
|
|
})
|
|
|
|
t.Run("should handle array values correctly", func(t *testing.T) {
|
|
optionsID := []string{model.NewId(), model.NewId(), model.NewId(), model.NewId()}
|
|
|
|
arrayField, err := model.NewCPAFieldFromPropertyField(&model.PropertyField{
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeMultiselect,
|
|
Attrs: model.StringInterface{
|
|
"options": []map[string]any{
|
|
{"id": optionsID[0], "name": "option1"},
|
|
{"id": optionsID[1], "name": "option2"},
|
|
{"id": optionsID[2], "name": "option3"},
|
|
{"id": optionsID[3], "name": "option4"},
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
createdArrayField, appErr := th.App.CreateCPAField(request.TestContext(t), arrayField)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdArrayField)
|
|
|
|
values := map[string]json.RawMessage{
|
|
createdArrayField.ID: json.RawMessage(fmt.Sprintf(`["%s", "%s", "%s"]`, optionsID[0], optionsID[1], optionsID[2])),
|
|
}
|
|
patchedValues, resp, err := th.Client.PatchCPAValuesForUser(context.Background(), th.BasicUser.Id, values)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, patchedValues)
|
|
|
|
var actualValues []string
|
|
require.NoError(t, json.Unmarshal(patchedValues[createdArrayField.ID], &actualValues))
|
|
require.Equal(t, optionsID[:3], actualValues)
|
|
|
|
// Test updating array values
|
|
values[createdArrayField.ID] = json.RawMessage(fmt.Sprintf(`["%s", "%s"]`, optionsID[2], optionsID[3]))
|
|
patchedValues, resp, err = th.Client.PatchCPAValuesForUser(context.Background(), th.BasicUser.Id, values)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
|
|
actualValues = nil
|
|
require.NoError(t, json.Unmarshal(patchedValues[createdArrayField.ID], &actualValues))
|
|
require.Equal(t, optionsID[2:4], actualValues)
|
|
})
|
|
|
|
t.Run("should fail if any of the values belongs to a field that is LDAP/SAML synced", func(t *testing.T) {
|
|
// Create a field with LDAP attribute
|
|
ldapField, err := model.NewCPAFieldFromPropertyField(&model.PropertyField{
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
Attrs: model.StringInterface{
|
|
model.CustomProfileAttributesPropertyAttrsLDAP: "ldap_attr",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
createdLDAPField, appErr := th.App.CreateCPAField(request.TestContext(t), ldapField)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdLDAPField)
|
|
|
|
// Create a field with SAML attribute
|
|
samlField, err := model.NewCPAFieldFromPropertyField(&model.PropertyField{
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
Attrs: model.StringInterface{
|
|
model.CustomProfileAttributesPropertyAttrsSAML: "saml_attr",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
createdSAMLField, appErr := th.App.CreateCPAField(request.TestContext(t), samlField)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdSAMLField)
|
|
|
|
// Test LDAP field
|
|
values := map[string]json.RawMessage{
|
|
createdLDAPField.ID: json.RawMessage(`"LDAP Value"`),
|
|
}
|
|
_, resp, err := th.Client.PatchCPAValuesForUser(context.Background(), th.BasicUser.Id, values)
|
|
CheckBadRequestStatus(t, resp)
|
|
require.Error(t, err)
|
|
CheckErrorID(t, err, "app.custom_profile_attributes.property_field_is_synced.app_error")
|
|
|
|
// Test SAML field
|
|
values = map[string]json.RawMessage{
|
|
createdSAMLField.ID: json.RawMessage(`"SAML Value"`),
|
|
}
|
|
_, resp, err = th.Client.PatchCPAValuesForUser(context.Background(), th.BasicUser.Id, values)
|
|
CheckBadRequestStatus(t, resp)
|
|
require.Error(t, err)
|
|
CheckErrorID(t, err, "app.custom_profile_attributes.property_field_is_synced.app_error")
|
|
|
|
// Test multiple fields with one being LDAP synced
|
|
values = map[string]json.RawMessage{
|
|
createdField.ID: json.RawMessage(`"Regular Value"`),
|
|
createdLDAPField.ID: json.RawMessage(`"LDAP Value"`),
|
|
}
|
|
_, resp, err = th.Client.PatchCPAValuesForUser(context.Background(), th.BasicUser.Id, values)
|
|
CheckBadRequestStatus(t, resp)
|
|
require.Error(t, err)
|
|
CheckErrorID(t, err, "app.custom_profile_attributes.property_field_is_synced.app_error")
|
|
})
|
|
|
|
t.Run("an invalid patch should be rejected", func(t *testing.T) {
|
|
field, err := model.NewCPAFieldFromPropertyField(&model.PropertyField{
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
createdField, appErr := th.App.CreateCPAField(request.TestContext(t), field)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdField)
|
|
|
|
// Create a value that's too long (over 64 characters)
|
|
tooLongValue := strings.Repeat("a", model.CPAValueTypeTextMaxLength+1)
|
|
values := map[string]json.RawMessage{
|
|
createdField.ID: json.RawMessage(fmt.Sprintf(`"%s"`, tooLongValue)),
|
|
}
|
|
|
|
_, resp, err := th.Client.PatchCPAValuesForUser(context.Background(), th.BasicUser.Id, values)
|
|
CheckBadRequestStatus(t, resp)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "Failed to validate property value")
|
|
})
|
|
|
|
t.Run("admin-managed fields", func(t *testing.T) {
|
|
// Create a managed field (only admins can create fields)
|
|
managedField := &model.PropertyField{
|
|
Name: "Managed Field",
|
|
Type: model.PropertyFieldTypeText,
|
|
Attrs: model.StringInterface{
|
|
model.CustomProfileAttributesPropertyAttrsManaged: "admin",
|
|
},
|
|
}
|
|
|
|
createdManagedField, resp, err := th.SystemAdminClient.CreateCPAField(context.Background(), managedField)
|
|
CheckCreatedStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, createdManagedField)
|
|
|
|
// Create a non-managed field for comparison
|
|
regularField := &model.PropertyField{
|
|
Name: "Regular Field",
|
|
Type: model.PropertyFieldTypeText,
|
|
}
|
|
|
|
createdRegularField, resp, err := th.SystemAdminClient.CreateCPAField(context.Background(), regularField)
|
|
CheckCreatedStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, createdRegularField)
|
|
|
|
t.Run("regular user cannot update managed field", func(t *testing.T) {
|
|
values := map[string]json.RawMessage{
|
|
createdManagedField.ID: json.RawMessage(`"Managed Value"`),
|
|
}
|
|
|
|
_, resp, err := th.Client.PatchCPAValuesForUser(context.Background(), th.BasicUser.Id, values)
|
|
CheckForbiddenStatus(t, resp)
|
|
require.Error(t, err)
|
|
CheckErrorID(t, err, "app.custom_profile_attributes.property_field_is_managed.app_error")
|
|
})
|
|
|
|
t.Run("regular user can update non-managed field", func(t *testing.T) {
|
|
values := map[string]json.RawMessage{
|
|
createdRegularField.ID: json.RawMessage(`"Regular Value"`),
|
|
}
|
|
|
|
patchedValues, resp, err := th.Client.PatchCPAValuesForUser(context.Background(), th.BasicUser.Id, values)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, patchedValues)
|
|
|
|
var actualValue string
|
|
require.NoError(t, json.Unmarshal(patchedValues[createdRegularField.ID], &actualValue))
|
|
require.Equal(t, "Regular Value", actualValue)
|
|
})
|
|
|
|
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
|
// Set initial value through the app layer that we will be replacing during the test
|
|
_, appErr := th.App.PatchCPAValue(request.TestContext(t), th.SystemAdminUser.Id, createdManagedField.ID, json.RawMessage(`"Initial Admin Value"`), true)
|
|
require.Nil(t, appErr)
|
|
|
|
values := map[string]json.RawMessage{
|
|
createdManagedField.ID: json.RawMessage(`"Admin Updated Value"`),
|
|
}
|
|
|
|
patchedValues, resp, err := client.PatchCPAValuesForUser(context.Background(), th.SystemAdminUser.Id, values)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, patchedValues)
|
|
|
|
var actualValue string
|
|
require.NoError(t, json.Unmarshal(patchedValues[createdManagedField.ID], &actualValue))
|
|
require.Equal(t, "Admin Updated Value", actualValue)
|
|
}, "system admin can update managed field")
|
|
|
|
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
|
values := map[string]json.RawMessage{
|
|
createdManagedField.ID: json.RawMessage(`"Admin Updated Managed Value For Other User"`),
|
|
}
|
|
|
|
patchedValues, resp, err := th.SystemAdminClient.PatchCPAValuesForUser(context.Background(), th.BasicUser.Id, values)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, patchedValues)
|
|
|
|
var actualValue string
|
|
require.NoError(t, json.Unmarshal(patchedValues[createdManagedField.ID], &actualValue))
|
|
require.Equal(t, "Admin Updated Managed Value For Other User", actualValue)
|
|
|
|
// Verify the value was actually set for the target user
|
|
userValues, resp, err := th.SystemAdminClient.ListCPAValues(context.Background(), th.BasicUser.Id)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, userValues)
|
|
|
|
var storedValue string
|
|
require.NoError(t, json.Unmarshal(userValues[createdManagedField.ID], &storedValue))
|
|
require.Equal(t, "Admin Updated Managed Value For Other User", storedValue)
|
|
}, "system admin can update managed field values for other users")
|
|
|
|
t.Run("a user should not be able to update other user's field values", func(t *testing.T) {
|
|
values := map[string]json.RawMessage{
|
|
createdRegularField.ID: json.RawMessage(`"Attempted Value For Other User"`),
|
|
}
|
|
|
|
// th.Client (BasicUser) trying to update th.BasicUser2's values should fail
|
|
_, resp, err := th.Client.PatchCPAValuesForUser(context.Background(), th.BasicUser2.Id, values)
|
|
CheckForbiddenStatus(t, resp)
|
|
require.Error(t, err)
|
|
CheckErrorID(t, err, "api.context.permissions.app_error")
|
|
})
|
|
|
|
t.Run("batch update with managed fields fails for regular user", func(t *testing.T) {
|
|
// First set some initial values to ensure we can verify they don't change
|
|
// Set initial values for both fields using th.App (admins can set managed field values)
|
|
_, appErr := th.App.PatchCPAValue(request.TestContext(t), th.BasicUser.Id, createdRegularField.ID, json.RawMessage(`"Initial Regular Value"`), false)
|
|
require.Nil(t, appErr)
|
|
|
|
_, appErr = th.App.PatchCPAValue(request.TestContext(t), th.BasicUser.Id, createdManagedField.ID, json.RawMessage(`"Initial Managed Value"`), true)
|
|
require.Nil(t, appErr)
|
|
|
|
// Try to batch update both managed and regular fields - this should fail
|
|
attemptedValues := map[string]json.RawMessage{
|
|
createdManagedField.ID: json.RawMessage(`"Managed Batch Value"`),
|
|
createdRegularField.ID: json.RawMessage(`"Regular Batch Value"`),
|
|
}
|
|
|
|
_, resp, err := th.Client.PatchCPAValuesForUser(context.Background(), th.BasicUser.Id, attemptedValues)
|
|
CheckForbiddenStatus(t, resp)
|
|
require.Error(t, err)
|
|
CheckErrorID(t, err, "app.custom_profile_attributes.property_field_is_managed.app_error")
|
|
|
|
// Verify that no values were updated when the batch operation failed
|
|
currentValues, appErr := th.App.ListCPAValues(request.TestContext(t), th.BasicUser.Id)
|
|
require.Nil(t, appErr)
|
|
|
|
// Check that values remain unchanged - both fields should retain their initial values
|
|
regularFieldHasOriginalValue := false
|
|
managedFieldHasOriginalValue := false
|
|
|
|
for _, value := range currentValues {
|
|
if value.FieldID == createdManagedField.ID {
|
|
var currentValue string
|
|
require.NoError(t, json.Unmarshal(value.Value, ¤tValue))
|
|
if currentValue == "Initial Managed Value" {
|
|
managedFieldHasOriginalValue = true
|
|
}
|
|
// Verify it's not the attempted update value
|
|
require.NotEqual(t, "Managed Batch Value", currentValue, "Managed field should not have been updated in failed batch operation")
|
|
}
|
|
if value.FieldID == createdRegularField.ID {
|
|
var currentValue string
|
|
require.NoError(t, json.Unmarshal(value.Value, ¤tValue))
|
|
if currentValue == "Initial Regular Value" {
|
|
regularFieldHasOriginalValue = true
|
|
}
|
|
// Verify it's not the attempted update value
|
|
require.NotEqual(t, "Regular Batch Value", currentValue, "Regular field should not have been updated in failed batch operation")
|
|
}
|
|
}
|
|
|
|
// Both fields should retain their original values after the failed batch operation
|
|
require.True(t, regularFieldHasOriginalValue, "Regular field should retain its original value")
|
|
require.True(t, managedFieldHasOriginalValue, "Managed field should retain its original value")
|
|
})
|
|
|
|
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
|
values := map[string]json.RawMessage{
|
|
createdManagedField.ID: json.RawMessage(`"Admin Managed Batch"`),
|
|
createdRegularField.ID: json.RawMessage(`"Admin Regular Batch"`),
|
|
}
|
|
|
|
patchedValues, resp, err := th.SystemAdminClient.PatchCPAValuesForUser(context.Background(), th.BasicUser.Id, values)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.Len(t, patchedValues, 2)
|
|
|
|
var managedValue, regularValue string
|
|
require.NoError(t, json.Unmarshal(patchedValues[createdManagedField.ID], &managedValue))
|
|
require.NoError(t, json.Unmarshal(patchedValues[createdRegularField.ID], ®ularValue))
|
|
require.Equal(t, "Admin Managed Batch", managedValue)
|
|
require.Equal(t, "Admin Regular Batch", regularValue)
|
|
}, "batch update with managed fields succeeds for admin")
|
|
})
|
|
}
|