diff --git a/server/channels/api4/custom_profile_attributes.go b/server/channels/api4/custom_profile_attributes.go
index 006cdced1f2..b4506130707 100644
--- a/server/channels/api4/custom_profile_attributes.go
+++ b/server/channels/api4/custom_profile_attributes.go
@@ -205,8 +205,6 @@ func patchCPAValues(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- // This check is unnecessary for now
- // Will be required when/if admins can patch other's values
userID := c.AppContext.Session().UserId
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), userID) {
c.SetPermissionError(model.PermissionEditOtherUsers)
@@ -223,6 +221,32 @@ func patchCPAValues(c *Context, w http.ResponseWriter, r *http.Request) {
defer c.LogAuditRec(auditRec)
model.AddEventParameterToAuditRec(auditRec, "user_id", userID)
+ // if the user is not an admin, we need to check that there are no
+ // admin-managed fields
+ if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
+ fields, appErr := c.App.ListCPAFields()
+ if appErr != nil {
+ c.Err = appErr
+ return
+ }
+
+ // Check if any of the fields being updated are admin-managed
+ for _, field := range fields {
+ if _, isBeingUpdated := updates[field.ID]; isBeingUpdated {
+ // Convert to CPAField to check if managed
+ cpaField, fErr := model.NewCPAFieldFromPropertyField(field)
+ if fErr != nil {
+ c.Err = model.NewAppError("Api4.patchCPAValues", "app.custom_profile_attributes.property_field_conversion.app_error", nil, "", http.StatusInternalServerError).Wrap(fErr)
+ return
+ }
+ if cpaField.IsAdminManaged() {
+ c.Err = model.NewAppError("Api4.patchCPAValues", "app.custom_profile_attributes.property_field_is_managed.app_error", nil, "", http.StatusForbidden)
+ return
+ }
+ }
+ }
+ }
+
results := make(map[string]json.RawMessage, len(updates))
for fieldID, rawValue := range updates {
patchedValue, appErr := c.App.PatchCPAValue(userID, fieldID, rawValue, false)
diff --git a/server/channels/api4/custom_profile_attributes_test.go b/server/channels/api4/custom_profile_attributes_test.go
index 7b2b69a27dc..2b6a55cc797 100644
--- a/server/channels/api4/custom_profile_attributes_test.go
+++ b/server/channels/api4/custom_profile_attributes_test.go
@@ -93,6 +93,25 @@ func TestCreateCPAField(t *testing.T) {
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) {
@@ -282,6 +301,48 @@ func TestPatchCPAField(t *testing.T) {
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(field)
+ require.Nil(t, appErr)
+ require.NotNil(t, createdField)
+
+ // Verify field is not isManaged initially
+ require.Empty(t, createdField.Attrs[model.CustomProfileAttributesPropertyAttrsManaged])
+
+ // 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) {
@@ -670,4 +731,144 @@ func TestPatchCPAValues(t *testing.T) {
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(th.BasicUser.Id, createdRegularField.ID, json.RawMessage(`"Initial Regular Value"`), false)
+ require.Nil(t, appErr)
+
+ _, appErr = th.App.PatchCPAValue(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(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)
+ })
+ })
}
diff --git a/server/i18n/en.json b/server/i18n/en.json
index 12bfc624f15..b79de1db8ce 100644
--- a/server/i18n/en.json
+++ b/server/i18n/en.json
@@ -5138,6 +5138,10 @@
"id": "app.custom_profile_attributes.property_field_delete.app_error",
"translation": "Unable to delete Custom Profile Attribute field"
},
+ {
+ "id": "app.custom_profile_attributes.property_field_is_managed.app_error",
+ "translation": "Cannot update value for an admin-managed Custom Profile Attribute field"
+ },
{
"id": "app.custom_profile_attributes.property_field_is_synced.app_error",
"translation": "Cannot update value for a synced Custom Profile Attribute field"
diff --git a/server/public/model/custom_profile_attributes.go b/server/public/model/custom_profile_attributes.go
index a8372343514..e697275a613 100644
--- a/server/public/model/custom_profile_attributes.go
+++ b/server/public/model/custom_profile_attributes.go
@@ -35,6 +35,7 @@ const (
CustomProfileAttributesPropertyAttrsVisibility = "visibility"
CustomProfileAttributesPropertyAttrsLDAP = "ldap"
CustomProfileAttributesPropertyAttrsSAML = "saml"
+ CustomProfileAttributesPropertyAttrsManaged = "managed"
// Value Types
CustomProfileAttributesValueTypeEmail = "email"
@@ -131,12 +132,17 @@ type CPAAttrs struct {
ValueType string `json:"value_type"`
LDAP string `json:"ldap"`
SAML string `json:"saml"`
+ Managed string `json:"managed"`
}
func (c *CPAField) IsSynced() bool {
return c.Attrs.LDAP != "" || c.Attrs.SAML != ""
}
+func (c *CPAField) IsAdminManaged() bool {
+ return c.Attrs.Managed == "admin"
+}
+
func (c *CPAField) ToPropertyField() *PropertyField {
pf := c.PropertyField
@@ -147,6 +153,7 @@ func (c *CPAField) ToPropertyField() *PropertyField {
PropertyFieldAttributeOptions: c.Attrs.Options,
CustomProfileAttributesPropertyAttrsLDAP: c.Attrs.LDAP,
CustomProfileAttributesPropertyAttrsSAML: c.Attrs.SAML,
+ CustomProfileAttributesPropertyAttrsManaged: c.Attrs.Managed,
}
return &pf
@@ -174,6 +181,12 @@ func (c *CPAField) SanitizeAndValidate() *AppError {
c.Attrs.SAML = ""
}
+ // Clear sync properties if managed is set (mutual exclusivity)
+ if c.IsAdminManaged() {
+ c.Attrs.LDAP = ""
+ c.Attrs.SAML = ""
+ }
+
switch c.Type {
case PropertyFieldTypeText:
if valueType := strings.TrimSpace(c.Attrs.ValueType); valueType != "" {
@@ -217,6 +230,17 @@ func (c *CPAField) SanitizeAndValidate() *AppError {
}
c.Attrs.Visibility = visibility
+ // Validate managed field
+ if managed := strings.TrimSpace(c.Attrs.Managed); managed != "" {
+ if managed != "admin" {
+ return NewAppError("SanitizeAndValidate", "app.custom_profile_attributes.sanitize_and_validate.app_error", map[string]any{
+ "AttributeName": CustomProfileAttributesPropertyAttrsManaged,
+ "Reason": "unknown managed type",
+ }, "", http.StatusBadRequest)
+ }
+ c.Attrs.Managed = managed
+ }
+
return nil
}
diff --git a/server/public/model/custom_profile_attributes_test.go b/server/public/model/custom_profile_attributes_test.go
index a3ed8833af2..d99317c57df 100644
--- a/server/public/model/custom_profile_attributes_test.go
+++ b/server/public/model/custom_profile_attributes_test.go
@@ -226,6 +226,68 @@ func TestCPAFieldToPropertyField(t *testing.T) {
}
})
}
+
+ // Test managed attribute functionality
+ t.Run("managed attribute", func(t *testing.T) {
+ managedTests := []struct {
+ name string
+ cpaField *CPAField
+ }{
+ {
+ name: "CPA field with managed attribute should include it in conversion",
+ cpaField: &CPAField{
+ PropertyField: PropertyField{
+ ID: NewId(),
+ GroupID: CustomProfileAttributesPropertyGroupName,
+ Name: "Managed Field",
+ Type: PropertyFieldTypeText,
+ CreateAt: GetMillis(),
+ UpdateAt: GetMillis(),
+ },
+ Attrs: CPAAttrs{
+ Visibility: CustomProfileAttributesVisibilityAlways,
+ SortOrder: 1,
+ Managed: "admin",
+ },
+ },
+ },
+ {
+ name: "CPA field with empty managed attribute should include it in conversion",
+ cpaField: &CPAField{
+ PropertyField: PropertyField{
+ ID: NewId(),
+ GroupID: CustomProfileAttributesPropertyGroupName,
+ Name: "Non-managed Field",
+ Type: PropertyFieldTypeText,
+ CreateAt: GetMillis(),
+ UpdateAt: GetMillis(),
+ },
+ Attrs: CPAAttrs{
+ Visibility: CustomProfileAttributesVisibilityWhenSet,
+ SortOrder: 2,
+ Managed: "",
+ },
+ },
+ },
+ }
+
+ for _, tt := range managedTests {
+ t.Run(tt.name, func(t *testing.T) {
+ pf := tt.cpaField.ToPropertyField()
+
+ require.NotNil(t, pf)
+
+ // Check that the PropertyField was copied correctly
+ assert.Equal(t, tt.cpaField.ID, pf.ID)
+ assert.Equal(t, tt.cpaField.GroupID, pf.GroupID)
+ assert.Equal(t, tt.cpaField.Name, pf.Name)
+ assert.Equal(t, tt.cpaField.Type, pf.Type)
+
+ // Check that the managed attribute was converted correctly
+ assert.Equal(t, tt.cpaField.Attrs.Managed, pf.Attrs[CustomProfileAttributesPropertyAttrsManaged])
+ })
+ }
+ })
}
func TestCustomProfileAttributeSelectOptionIsValid(t *testing.T) {
@@ -695,6 +757,134 @@ func TestCPAField_SanitizeAndValidate(t *testing.T) {
}
})
}
+
+ // Test managed fields functionality
+ t.Run("managed fields", func(t *testing.T) {
+ managedTests := []struct {
+ name string
+ field *CPAField
+ expectError bool
+ errorId string
+ expectedAttrs CPAAttrs
+ }{
+ {
+ name: "valid managed field with admin value",
+ field: &CPAField{
+ PropertyField: PropertyField{
+ Type: PropertyFieldTypeText,
+ },
+ Attrs: CPAAttrs{
+ Managed: "admin",
+ },
+ },
+ expectError: false,
+ expectedAttrs: CPAAttrs{
+ Visibility: CustomProfileAttributesVisibilityDefault,
+ Managed: "admin",
+ },
+ },
+ {
+ name: "managed field with whitespace should be trimmed",
+ field: &CPAField{
+ PropertyField: PropertyField{
+ Type: PropertyFieldTypeText,
+ },
+ Attrs: CPAAttrs{
+ Managed: " admin ",
+ },
+ },
+ expectError: false,
+ expectedAttrs: CPAAttrs{
+ Visibility: CustomProfileAttributesVisibilityDefault,
+ Managed: "admin",
+ },
+ },
+ {
+ name: "field with empty managed should be allowed",
+ field: &CPAField{
+ PropertyField: PropertyField{
+ Type: PropertyFieldTypeText,
+ },
+ Attrs: CPAAttrs{
+ Managed: "",
+ },
+ },
+ expectError: false,
+ expectedAttrs: CPAAttrs{
+ Visibility: CustomProfileAttributesVisibilityDefault,
+ Managed: "",
+ },
+ },
+ {
+ name: "field with invalid managed value should fail",
+ field: &CPAField{
+ PropertyField: PropertyField{
+ Type: PropertyFieldTypeText,
+ },
+ Attrs: CPAAttrs{
+ Managed: "invalid",
+ },
+ },
+ expectError: true,
+ errorId: "app.custom_profile_attributes.sanitize_and_validate.app_error",
+ },
+ {
+ name: "managed field should clear LDAP sync properties",
+ field: &CPAField{
+ PropertyField: PropertyField{
+ Type: PropertyFieldTypeText,
+ },
+ Attrs: CPAAttrs{
+ Managed: "admin",
+ LDAP: "ldap_attribute",
+ SAML: "saml_attribute",
+ },
+ },
+ expectError: false,
+ expectedAttrs: CPAAttrs{
+ Visibility: CustomProfileAttributesVisibilityDefault,
+ Managed: "admin",
+ LDAP: "", // Should be cleared
+ SAML: "", // Should be cleared
+ },
+ },
+ {
+ name: "managed field should clear sync properties even when field supports syncing",
+ field: &CPAField{
+ PropertyField: PropertyField{
+ Type: PropertyFieldTypeText, // Text fields support syncing
+ },
+ Attrs: CPAAttrs{
+ Managed: "admin",
+ LDAP: "ldap_attribute",
+ },
+ },
+ expectError: false,
+ expectedAttrs: CPAAttrs{
+ Visibility: CustomProfileAttributesVisibilityDefault,
+ Managed: "admin",
+ LDAP: "", // Should be cleared due to mutual exclusivity
+ SAML: "",
+ },
+ },
+ }
+
+ for _, tt := range managedTests {
+ t.Run(tt.name, func(t *testing.T) {
+ err := tt.field.SanitizeAndValidate()
+ if tt.expectError {
+ require.NotNil(t, err)
+ require.Equal(t, tt.errorId, err.Id)
+ } else {
+ require.Nil(t, err)
+ assert.Equal(t, tt.expectedAttrs.Visibility, tt.field.Attrs.Visibility)
+ assert.Equal(t, tt.expectedAttrs.Managed, tt.field.Attrs.Managed)
+ assert.Equal(t, tt.expectedAttrs.LDAP, tt.field.Attrs.LDAP)
+ assert.Equal(t, tt.expectedAttrs.SAML, tt.field.Attrs.SAML)
+ }
+ })
+ }
+ })
}
func TestSanitizeAndValidatePropertyValue(t *testing.T) {
@@ -893,3 +1083,53 @@ func TestSanitizeAndValidatePropertyValue(t *testing.T) {
})
})
}
+
+func TestCPAField_IsAdminManaged(t *testing.T) {
+ tests := []struct {
+ name string
+ field *CPAField
+ expected bool
+ }{
+ {
+ name: "field with managed admin attribute should return true",
+ field: &CPAField{
+ Attrs: CPAAttrs{
+ Managed: "admin",
+ },
+ },
+ expected: true,
+ },
+ {
+ name: "field with empty managed attribute should return false",
+ field: &CPAField{
+ Attrs: CPAAttrs{
+ Managed: "",
+ },
+ },
+ expected: false,
+ },
+ {
+ name: "field with non-admin managed attribute should return false",
+ field: &CPAField{
+ Attrs: CPAAttrs{
+ Managed: "user",
+ },
+ },
+ expected: false,
+ },
+ {
+ name: "field with no managed attribute should return false",
+ field: &CPAField{
+ Attrs: CPAAttrs{},
+ },
+ expected: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result := tt.field.IsAdminManaged()
+ assert.Equal(t, tt.expected, result)
+ })
+ }
+}
diff --git a/webapp/channels/src/components/user_settings/general/user_settings_general.test.tsx b/webapp/channels/src/components/user_settings/general/user_settings_general.test.tsx
index 2c68251e088..23f507c817e 100644
--- a/webapp/channels/src/components/user_settings/general/user_settings_general.test.tsx
+++ b/webapp/channels/src/components/user_settings/general/user_settings_general.test.tsx
@@ -832,4 +832,50 @@ describe('components/user_settings/general/UserSettingsGeneral', () => {
expect(saveCustomProfileAttribute).toHaveBeenCalledWith('user_id', 'field1', 'test@example.com');
});
+
+ test('should not show custom attribute input field when field is admin-managed', async () => {
+ const adminManagedAttribute: UserPropertyField = {
+ ...customProfileAttribute,
+ attrs: {
+ ...customProfileAttribute.attrs,
+ managed: 'admin',
+ },
+ };
+
+ const props = {
+ ...requiredProps,
+ enableCustomProfileAttributes: true,
+ customProfileAttributeFields: [adminManagedAttribute],
+ user: {...user},
+ activeSection: 'customAttribute_field1',
+ };
+
+ renderWithContext();
+ expect(screen.queryByRole('button', {name: 'Save'})).not.toBeInTheDocument();
+ expect(screen.queryByRole('textbox', {name: adminManagedAttribute.name})).not.toBeInTheDocument();
+ expect(await screen.findByText('This field can only be changed by an administrator.')).toBeInTheDocument();
+ });
+
+ test('should show custom attribute input field when field is not admin-managed', async () => {
+ const regularAttribute: UserPropertyField = {
+ ...customProfileAttribute,
+ attrs: {
+ ...customProfileAttribute.attrs,
+ managed: '',
+ },
+ };
+
+ const props = {
+ ...requiredProps,
+ enableCustomProfileAttributes: true,
+ customProfileAttributeFields: [regularAttribute],
+ user: {...user},
+ activeSection: 'customAttribute_field1',
+ };
+
+ renderWithContext();
+ expect(await screen.getByRole('button', {name: 'Save'})).toBeInTheDocument();
+ expect(screen.getByRole('textbox', {name: regularAttribute.name})).toBeInTheDocument();
+ expect(screen.queryByText('This field can only be changed by an administrator.')).not.toBeInTheDocument();
+ });
});
diff --git a/webapp/channels/src/components/user_settings/general/user_settings_general.tsx b/webapp/channels/src/components/user_settings/general/user_settings_general.tsx
index cb6fc9c2daa..59a82a14032 100644
--- a/webapp/channels/src/components/user_settings/general/user_settings_general.tsx
+++ b/webapp/channels/src/components/user_settings/general/user_settings_general.tsx
@@ -1514,6 +1514,15 @@ export class UserSettingsGeneralTab extends PureComponent {
/>
);
+ } else if (attribute.attrs?.managed === 'admin') {
+ extraInfo = (
+
+
+
+ );
} else {
let attributeLabel: JSX.Element | string = (
attribute.name
diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json
index 13c3f63a6f0..721bc6a92d4 100644
--- a/webapp/channels/src/i18n/en.json
+++ b/webapp/channels/src/i18n/en.json
@@ -6066,6 +6066,7 @@
"user.settings.general.emptyPassword": "Please enter your current password.",
"user.settings.general.emptyPosition": "Click 'Edit' to add your job title / position",
"user.settings.general.field_handled_externally": "This field is handled through your login provider. If you want to change it, you need to do so through your login provider.",
+ "user.settings.general.field_managed_by_admin": "This field can only be changed by an administrator.",
"user.settings.general.firstName": "First Name",
"user.settings.general.fullName": "Full Name",
"user.settings.general.imageTooLarge": "Unable to upload profile image. File is too large.",