From ac3d087d2d4e6828a872b75d8198992e90ddc72d Mon Sep 17 00:00:00 2001 From: Miguel de la Cruz Date: Fri, 22 Aug 2025 14:33:20 +0200 Subject: [PATCH] Adds admin managed property fields (#33662) * Adds admin managed property fields * Fix linter * Adds extra tests * Update server/public/model/custom_profile_attributes.go Co-authored-by: Caleb Roseland * Fix linter --------- Co-authored-by: Miguel de la Cruz Co-authored-by: Caleb Roseland --- .../api4/custom_profile_attributes.go | 28 +- .../api4/custom_profile_attributes_test.go | 201 +++++++++++++++ server/i18n/en.json | 4 + .../public/model/custom_profile_attributes.go | 24 ++ .../model/custom_profile_attributes_test.go | 240 ++++++++++++++++++ .../general/user_settings_general.test.tsx | 46 ++++ .../general/user_settings_general.tsx | 9 + webapp/channels/src/i18n/en.json | 1 + 8 files changed, 551 insertions(+), 2 deletions(-) diff --git a/server/channels/api4/custom_profile_attributes.go b/server/channels/api4/custom_profile_attributes.go index 006cdced1f2f..b45061307079 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 7b2b69a27dc0..2b6a55cc7977 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 12bfc624f159..b79de1db8cef 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 a8372343514f..e697275a6136 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 a3ed8833af2f..d99317c57df2 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 2c68251e0880..23f507c817e0 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 cb6fc9c2daa5..59a82a140326 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 13c3f63a6f0d..721bc6a92d4e 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.",