mirror of
https://github.com/mattermost/mattermost.git
synced 2026-08-27 05:37:15 -05:00
Adds Custom Profile Attributes value commands to mmctl (#33881)
Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
This commit is contained in:
co-authored by
Miguel de la Cruz
parent
e8288b8b71
commit
aad2fa1461
@@ -250,7 +250,7 @@
|
||||
type: string
|
||||
value:
|
||||
oneOf:
|
||||
- type: string
|
||||
- type: string
|
||||
- type: array
|
||||
items:
|
||||
type: string
|
||||
@@ -349,6 +349,65 @@
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"403":
|
||||
$ref: "#/components/responses/Forbidden"
|
||||
patch:
|
||||
tags:
|
||||
- custom profile attributes
|
||||
summary: Update custom profile attribute values for a user
|
||||
description: |
|
||||
Update Custom Profile Attribute field values for a specific user.
|
||||
|
||||
_This endpoint is experimental._
|
||||
|
||||
__Minimum server version__: 11
|
||||
|
||||
##### Permissions
|
||||
Must have permission to edit the user. Users can only edit their own CPA values unless they are system administrators.
|
||||
parameters:
|
||||
- name: user_id
|
||||
in: path
|
||||
description: User GUID
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
description: Custom Profile Attribute values that are to be updated
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
value:
|
||||
oneOf:
|
||||
- type: string
|
||||
- type: array
|
||||
items:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Custom profile attribute values updated successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
value:
|
||||
oneOf:
|
||||
- type: string
|
||||
- type: array
|
||||
items:
|
||||
type: string
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
|
||||
@@ -21,6 +21,7 @@ func (api *API) InitCustomProfileAttributes() {
|
||||
api.BaseRoutes.User.Handle("/custom_profile_attributes", api.APISessionRequired(listCPAValues)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.CustomProfileAttributesValues.Handle("", api.APISessionRequired(patchCPAValues)).Methods(http.MethodPatch)
|
||||
api.BaseRoutes.CustomProfileAttributes.Handle("/group", api.APISessionRequired(getCPAGroup)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.User.Handle("/custom_profile_attributes", api.APISessionRequired(patchCPAValuesForUser)).Methods(http.MethodPatch)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,3 +298,75 @@ func listCPAValues(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func patchCPAValuesForUser(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !model.MinimumEnterpriseLicense(c.App.Channels().License()) {
|
||||
c.Err = model.NewAppError("Api4.patchCPAValues", "api.custom_profile_attributes.license_error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Get userID from URL
|
||||
c.RequireUserId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
userID := c.Params.UserId
|
||||
|
||||
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), userID) {
|
||||
c.SetPermissionError(model.PermissionEditOtherUsers)
|
||||
return
|
||||
}
|
||||
|
||||
var updates map[string]json.RawMessage
|
||||
if err := json.NewDecoder(r.Body).Decode(&updates); err != nil {
|
||||
c.SetInvalidParamWithErr("value", err)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventPatchCPAValues, model.AuditStatusFail)
|
||||
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)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
results[fieldID] = patchedValue.Value
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
auditRec.AddEventObjectType("patchCPAValues")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(results); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,5 +13,6 @@ func (api *API) InitCustomProfileAttributesLocal() {
|
||||
api.BaseRoutes.CustomProfileAttributesField.Handle("", api.APILocal(deleteCPAField)).Methods(http.MethodDelete)
|
||||
api.BaseRoutes.User.Handle("/custom_profile_attributes", api.APISessionRequired(listCPAValues)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.CustomProfileAttributesValues.Handle("", api.APISessionRequired(patchCPAValues)).Methods(http.MethodPatch)
|
||||
api.BaseRoutes.User.Handle("/custom_profile_attributes", api.APISessionRequired(patchCPAValuesForUser)).Methods(http.MethodPatch)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -872,3 +872,406 @@ func TestPatchCPAValues(t *testing.T) {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestPatchCPAValuesForUser(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
|
||||
th := SetupConfig(t, func(cfg *model.Config) {
|
||||
cfg.FeatureFlags.CustomProfileAttributes = true
|
||||
}).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
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)
|
||||
|
||||
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(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(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(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(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)
|
||||
})
|
||||
|
||||
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.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)
|
||||
})
|
||||
|
||||
t.Run("system admin can update managed field values for other users", func(t *testing.T) {
|
||||
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)
|
||||
})
|
||||
|
||||
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(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.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(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.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)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -166,14 +166,11 @@ type Client interface {
|
||||
DeletePreferences(ctx context.Context, userId string, preferences model.Preferences) (*model.Response, error)
|
||||
PermanentDeletePost(ctx context.Context, postID string) (*model.Response, error)
|
||||
DeletePost(ctx context.Context, postId string) (*model.Response, error)
|
||||
|
||||
// CPA Field Management
|
||||
ListCPAFields(ctx context.Context) ([]*model.PropertyField, *model.Response, error)
|
||||
CreateCPAField(ctx context.Context, field *model.PropertyField) (*model.PropertyField, *model.Response, error)
|
||||
PatchCPAField(ctx context.Context, fieldID string, patch *model.PropertyFieldPatch) (*model.PropertyField, *model.Response, error)
|
||||
DeleteCPAField(ctx context.Context, fieldID string) (*model.Response, error)
|
||||
|
||||
// CPA Value Management
|
||||
ListCPAValues(ctx context.Context, userID string) (map[string]json.RawMessage, *model.Response, error)
|
||||
PatchCPAValues(ctx context.Context, values map[string]json.RawMessage) (map[string]json.RawMessage, *model.Response, error)
|
||||
PatchCPAValuesForUser(ctx context.Context, userID string, values map[string]json.RawMessage) (map[string]json.RawMessage, *model.Response, error)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
var CPACmd = &cobra.Command{
|
||||
Use: "cpa",
|
||||
Short: "Management of Custom Profile Attributes",
|
||||
Long: "Management of Custom Profile Attributes (CPA) fields.",
|
||||
Long: "Management of Custom Profile Attributes (CPA) fields and values.",
|
||||
}
|
||||
|
||||
var CPAFieldCmd = &cobra.Command{
|
||||
@@ -24,9 +24,16 @@ var CPAFieldCmd = &cobra.Command{
|
||||
Long: "Create, list, edit, and delete Custom Profile Attribute fields.",
|
||||
}
|
||||
|
||||
var CPAValueCmd = &cobra.Command{
|
||||
Use: "value",
|
||||
Short: "Management of CPA values",
|
||||
Long: "List, set, and delete Custom Profile Attribute values for users.",
|
||||
}
|
||||
|
||||
func init() {
|
||||
CPACmd.AddCommand(
|
||||
CPAFieldCmd,
|
||||
CPAValueCmd,
|
||||
)
|
||||
|
||||
RootCmd.AddCommand(CPACmd)
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var CPAValueListCmd = &cobra.Command{
|
||||
Use: "list [user]",
|
||||
Short: "List CPA values for a user",
|
||||
Long: "List all Custom Profile Attribute values for a specific user.",
|
||||
Example: ` cpa value list john.doe@company.com
|
||||
cpa value list johndoe`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: withClient(cpaValueListCmdF),
|
||||
}
|
||||
|
||||
var CPAValueSetCmd = &cobra.Command{
|
||||
Use: "set [user] [field-id]",
|
||||
Short: "Set a CPA value for a user",
|
||||
Long: "Set a Custom Profile Attribute field value for a specific user.",
|
||||
Example: ` cpa value set john.doe@company.com kx8m2w4r9p3q7n5t1j6h8s4c9e --value "Engineering"
|
||||
cpa value set johndoe q7n3t8w5r2m9k4x6p1j3h7s8c4 --value "Go" --value "React" --value "Python"
|
||||
cpa value set user123 w9r5t2n8k4x7p3q6m1j9h4s7c2 --value "Senior"`,
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: withClient(cpaValueSetCmdF),
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Set flags
|
||||
CPAValueSetCmd.Flags().StringSlice("value", []string{}, "Value(s) to set for the field. Can be specified multiple times for multiselect/multiuser fields")
|
||||
_ = CPAValueSetCmd.MarkFlagRequired("value")
|
||||
|
||||
// Add subcommands to CPAValueCmd
|
||||
CPAValueCmd.AddCommand(
|
||||
CPAValueListCmd,
|
||||
CPAValueSetCmd,
|
||||
)
|
||||
}
|
||||
|
||||
func cpaValueListCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
userArg := args[0]
|
||||
|
||||
// Resolve user
|
||||
user, err := getUserFromArg(c, userArg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get all values for the user
|
||||
values, _, err := c.ListCPAValues(context.TODO(), user.Id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get CPA values for user %s: %w", user.Username, err)
|
||||
}
|
||||
|
||||
for fieldID, value := range values {
|
||||
keypair := map[string]any{
|
||||
fieldID: value,
|
||||
}
|
||||
printer.PrintT("{{range $k, $v := .}}FieldID: {{$k}}, Value: {{printf \"%s\" $v}}{{end}}", keypair)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cpaValueSetCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
userArg := args[0]
|
||||
fieldID := args[1]
|
||||
|
||||
// Get values from flag
|
||||
values, err := cmd.Flags().GetStringSlice("value")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get values: %w", err)
|
||||
}
|
||||
|
||||
// Resolve user
|
||||
user, err := getUserFromArg(c, userArg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get field info to validate
|
||||
fields, _, err := c.ListCPAFields(context.TODO())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get CPA fields: %w", err)
|
||||
}
|
||||
|
||||
var targetField *model.PropertyField
|
||||
for _, field := range fields {
|
||||
if field.ID == fieldID {
|
||||
targetField = field
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if targetField == nil {
|
||||
return fmt.Errorf("field %s not found", fieldID)
|
||||
}
|
||||
|
||||
// Resolve option names to IDs for select/multiselect fields
|
||||
resolvedValues, err := resolveOptionNamesToIDs(targetField, values)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to resolve option values: %w", err)
|
||||
}
|
||||
|
||||
// Prepare the value for marshaling
|
||||
var valueToMarshal any
|
||||
if len(resolvedValues) == 1 {
|
||||
// Single value
|
||||
valueToMarshal = resolvedValues[0]
|
||||
} else {
|
||||
// Multiple values
|
||||
valueToMarshal = resolvedValues
|
||||
}
|
||||
|
||||
// Set the value using PatchCPAValues
|
||||
valueJSON, err := json.Marshal(valueToMarshal)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal value: %w", err)
|
||||
}
|
||||
|
||||
patchValues := map[string]json.RawMessage{
|
||||
fieldID: valueJSON,
|
||||
}
|
||||
|
||||
updatedValues, _, err := c.PatchCPAValuesForUser(context.TODO(), user.Id, patchValues)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set CPA value: %w", err)
|
||||
}
|
||||
|
||||
printer.SetSingle(true)
|
||||
printer.Print(updatedValues)
|
||||
|
||||
valueStr := fmt.Sprintf("%v", valueToMarshal)
|
||||
fmt.Printf("Successfully set CPA value for user %s, field %s: %s\n", user.Username, targetField.Name, valueStr)
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveOptionNamesToIDs converts option names to option IDs for select/multiselect fields
|
||||
func resolveOptionNamesToIDs(field *model.PropertyField, values []string) ([]string, error) {
|
||||
// For non-select fields, return values as-is
|
||||
if field.Type != model.PropertyFieldTypeSelect && field.Type != model.PropertyFieldTypeMultiselect {
|
||||
return values, nil
|
||||
}
|
||||
|
||||
// Convert PropertyField to CPAField to access options
|
||||
cpaField, err := model.NewCPAFieldFromPropertyField(field)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resolvedValues []string
|
||||
for _, value := range values {
|
||||
optionID := findOptionIDByName(cpaField.Attrs.Options, value)
|
||||
if optionID == "" {
|
||||
// If not found as name, assume it's already an ID (backward compatibility)
|
||||
resolvedValues = append(resolvedValues, value)
|
||||
} else {
|
||||
resolvedValues = append(resolvedValues, optionID)
|
||||
}
|
||||
}
|
||||
return resolvedValues, nil
|
||||
}
|
||||
|
||||
// findOptionIDByName finds the option ID for a given option name
|
||||
func findOptionIDByName(options []*model.CustomProfileAttributesSelectOption, name string) string {
|
||||
for _, option := range options {
|
||||
if option.Name == name {
|
||||
return option.ID
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
)
|
||||
|
||||
// cleanCPAValuesForUser removes all CPA values for a user
|
||||
func (s *MmctlE2ETestSuite) cleanCPAValuesForUser(userID string) {
|
||||
existingValues, appErr := s.th.App.ListCPAValues(userID)
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
// Clear all existing values by setting them to null
|
||||
updates := make(map[string]json.RawMessage)
|
||||
for _, value := range existingValues {
|
||||
updates[value.FieldID] = json.RawMessage("null")
|
||||
}
|
||||
|
||||
if len(updates) > 0 {
|
||||
_, appErr = s.th.App.PatchCPAValues(userID, updates, false)
|
||||
s.Require().Nil(appErr)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestCPAValueList() {
|
||||
s.SetupEnterpriseTestHelper().InitBasic()
|
||||
s.th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
|
||||
|
||||
s.Run("List CPA values with no values", func() {
|
||||
c := s.th.SystemAdminClient
|
||||
printer.Clean()
|
||||
s.cleanCPAFields()
|
||||
s.cleanCPAValuesForUser(s.th.BasicUser.Id)
|
||||
|
||||
// Test listing when no values are set
|
||||
err := cpaValueListCmdF(c, &cobra.Command{}, []string{s.th.BasicUser.Email})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("List CPA values with existing values", func() {
|
||||
c := s.th.SystemAdminClient
|
||||
printer.Clean()
|
||||
s.cleanCPAFields()
|
||||
s.cleanCPAValuesForUser(s.th.BasicUser.Id)
|
||||
|
||||
// Create a text field
|
||||
textField := &model.CPAField{
|
||||
PropertyField: model.PropertyField{
|
||||
Name: "Department",
|
||||
Type: model.PropertyFieldTypeText,
|
||||
TargetType: "user",
|
||||
},
|
||||
Attrs: model.CPAAttrs{
|
||||
Managed: "",
|
||||
},
|
||||
}
|
||||
|
||||
createdField, appErr := s.th.App.CreateCPAField(textField)
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
// Set a text value using the app layer
|
||||
updates := map[string]json.RawMessage{
|
||||
createdField.ID: json.RawMessage(`"Engineering"`),
|
||||
}
|
||||
_, appErr = s.th.App.PatchCPAValues(s.th.BasicUser.Id, updates, false)
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
// Test listing the values
|
||||
err := cpaValueListCmdF(c, &cobra.Command{}, []string{s.th.BasicUser.Email})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
|
||||
// Check that the value returned corresponds to Engineering
|
||||
outputMap := printer.GetLines()[0].(map[string]any)
|
||||
// The output contains field ID as key and value as the map value
|
||||
s.Require().Contains(outputMap, createdField.ID)
|
||||
s.Require().Equal(`"Engineering"`, string(outputMap[createdField.ID].(json.RawMessage)))
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestCPAValueSet() {
|
||||
s.SetupEnterpriseTestHelper().InitBasic()
|
||||
s.th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
|
||||
|
||||
s.Run("Set value for text type field", func() {
|
||||
c := s.th.SystemAdminClient
|
||||
printer.Clean()
|
||||
s.cleanCPAFields()
|
||||
s.cleanCPAValuesForUser(s.th.BasicUser.Id)
|
||||
|
||||
// Create a text field
|
||||
textField := &model.CPAField{
|
||||
PropertyField: model.PropertyField{
|
||||
Name: "Department",
|
||||
Type: model.PropertyFieldTypeText,
|
||||
TargetType: "user",
|
||||
},
|
||||
Attrs: model.CPAAttrs{
|
||||
Managed: "",
|
||||
},
|
||||
}
|
||||
|
||||
createdField, appErr := s.th.App.CreateCPAField(textField)
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
// Set a text value
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().StringSlice("value", []string{}, "")
|
||||
err := cmd.Flags().Set("value", "Engineering")
|
||||
s.Require().Nil(err)
|
||||
|
||||
err = cpaValueSetCmdF(c, cmd, []string{s.th.BasicUser.Email, createdField.ID})
|
||||
s.Require().Nil(err)
|
||||
|
||||
// Verify the value was set
|
||||
values, appErr := s.th.App.ListCPAValues(s.th.BasicUser.Id)
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Len(values, 1)
|
||||
s.Require().Equal(createdField.ID, values[0].FieldID)
|
||||
s.Require().Equal(`"Engineering"`, string(values[0].Value))
|
||||
})
|
||||
|
||||
s.Run("Set value for select type field", func() {
|
||||
c := s.th.SystemAdminClient
|
||||
printer.Clean()
|
||||
s.cleanCPAFields()
|
||||
s.cleanCPAValuesForUser(s.th.BasicUser.Id)
|
||||
|
||||
// Create a select field with options
|
||||
selectField := &model.CPAField{
|
||||
PropertyField: model.PropertyField{
|
||||
Name: "Level",
|
||||
Type: model.PropertyFieldTypeSelect,
|
||||
TargetType: "user",
|
||||
},
|
||||
Attrs: model.CPAAttrs{
|
||||
Managed: "",
|
||||
Options: []*model.CustomProfileAttributesSelectOption{
|
||||
{ID: model.NewId(), Name: "Junior"},
|
||||
{ID: model.NewId(), Name: "Senior"},
|
||||
{ID: model.NewId(), Name: "Lead"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
createdField, appErr := s.th.App.CreateCPAField(selectField)
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
// Convert to CPAField to access options
|
||||
cpaField, err := model.NewCPAFieldFromPropertyField(createdField)
|
||||
s.Require().Nil(err)
|
||||
|
||||
// Set a select value using the option name
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().StringSlice("value", []string{}, "")
|
||||
err = cmd.Flags().Set("value", "Senior")
|
||||
s.Require().Nil(err)
|
||||
|
||||
err = cpaValueSetCmdF(c, cmd, []string{s.th.BasicUser.Email, createdField.ID})
|
||||
s.Require().Nil(err)
|
||||
|
||||
// Verify the value was set (should be stored as option ID)
|
||||
values, appErr := s.th.App.ListCPAValues(s.th.BasicUser.Id)
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Len(values, 1)
|
||||
s.Require().Equal(createdField.ID, values[0].FieldID)
|
||||
|
||||
// Find the Senior option ID for verification
|
||||
var seniorOptionID string
|
||||
for _, option := range cpaField.Attrs.Options {
|
||||
if option.Name == "Senior" {
|
||||
seniorOptionID = option.ID
|
||||
break
|
||||
}
|
||||
}
|
||||
s.Require().Equal(`"`+seniorOptionID+`"`, string(values[0].Value))
|
||||
})
|
||||
|
||||
s.Run("Set value for multiselect type field", func() {
|
||||
c := s.th.SystemAdminClient
|
||||
printer.Clean()
|
||||
s.cleanCPAFields()
|
||||
s.cleanCPAValuesForUser(s.th.BasicUser.Id)
|
||||
|
||||
// Create a multiselect field with options
|
||||
multiselectField := &model.CPAField{
|
||||
PropertyField: model.PropertyField{
|
||||
Name: "Skills",
|
||||
Type: model.PropertyFieldTypeMultiselect,
|
||||
TargetType: "user",
|
||||
},
|
||||
Attrs: model.CPAAttrs{
|
||||
Managed: "",
|
||||
Options: []*model.CustomProfileAttributesSelectOption{
|
||||
{ID: model.NewId(), Name: "Go"},
|
||||
{ID: model.NewId(), Name: "React"},
|
||||
{ID: model.NewId(), Name: "Python"},
|
||||
{ID: model.NewId(), Name: "JavaScript"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
createdField, appErr := s.th.App.CreateCPAField(multiselectField)
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
// Convert to CPAField to access options
|
||||
cpaField, err := model.NewCPAFieldFromPropertyField(createdField)
|
||||
s.Require().Nil(err)
|
||||
|
||||
// Set multiple values using option names
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().StringSlice("value", []string{}, "")
|
||||
|
||||
err = cmd.Flags().Set("value", "Go")
|
||||
s.Require().Nil(err)
|
||||
err = cmd.Flags().Set("value", "React")
|
||||
s.Require().Nil(err)
|
||||
err = cmd.Flags().Set("value", "Python")
|
||||
s.Require().Nil(err)
|
||||
|
||||
err = cpaValueSetCmdF(c, cmd, []string{s.th.BasicUser.Email, createdField.ID})
|
||||
s.Require().Nil(err)
|
||||
|
||||
// Verify the values were set (should be stored as option IDs)
|
||||
values, appErr := s.th.App.ListCPAValues(s.th.BasicUser.Id)
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Len(values, 1)
|
||||
s.Require().Equal(createdField.ID, values[0].FieldID)
|
||||
|
||||
// Find the option IDs for verification
|
||||
var goOptionID, reactOptionID, pythonOptionID string
|
||||
for _, option := range cpaField.Attrs.Options {
|
||||
switch option.Name {
|
||||
case "Go":
|
||||
goOptionID = option.ID
|
||||
case "React":
|
||||
reactOptionID = option.ID
|
||||
case "Python":
|
||||
pythonOptionID = option.ID
|
||||
}
|
||||
}
|
||||
|
||||
// The multiselect values should be stored as an array of option IDs
|
||||
// The JSON serialization may include spaces, so we need to compare the content, not exact string
|
||||
actualValue := string(values[0].Value)
|
||||
s.Require().Contains(actualValue, goOptionID)
|
||||
s.Require().Contains(actualValue, reactOptionID)
|
||||
s.Require().Contains(actualValue, pythonOptionID)
|
||||
s.Require().Contains(actualValue, "[")
|
||||
s.Require().Contains(actualValue, "]")
|
||||
})
|
||||
|
||||
s.Run("Set value for user type field", func() {
|
||||
c := s.th.SystemAdminClient
|
||||
printer.Clean()
|
||||
s.cleanCPAFields()
|
||||
s.cleanCPAValuesForUser(s.th.BasicUser.Id)
|
||||
|
||||
// Create a user field
|
||||
userField := &model.CPAField{
|
||||
PropertyField: model.PropertyField{
|
||||
Name: "Manager",
|
||||
Type: model.PropertyFieldTypeUser,
|
||||
TargetType: "user",
|
||||
},
|
||||
Attrs: model.CPAAttrs{
|
||||
Managed: "",
|
||||
},
|
||||
}
|
||||
|
||||
createdField, appErr := s.th.App.CreateCPAField(userField)
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
// Set a user value using the system admin user ID
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().StringSlice("value", []string{}, "")
|
||||
err := cmd.Flags().Set("value", s.th.SystemAdminUser.Id)
|
||||
s.Require().Nil(err)
|
||||
|
||||
err = cpaValueSetCmdF(c, cmd, []string{s.th.BasicUser.Email, createdField.ID})
|
||||
s.Require().Nil(err)
|
||||
|
||||
// Verify the value was set
|
||||
values, appErr := s.th.App.ListCPAValues(s.th.BasicUser.Id)
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Len(values, 1)
|
||||
s.Require().Equal(createdField.ID, values[0].FieldID)
|
||||
s.Require().Equal(`"`+s.th.SystemAdminUser.Id+`"`, string(values[0].Value))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestCPAValueListCmd() {
|
||||
s.Run("Should list all CPA values with plain text output format", func() {
|
||||
printer.Clean()
|
||||
printer.SetFormat(printer.FormatPlain)
|
||||
|
||||
mockUser := &model.User{
|
||||
Id: "user123",
|
||||
Username: "testuser",
|
||||
}
|
||||
|
||||
mockValues := map[string]json.RawMessage{
|
||||
"field1": json.RawMessage(`"Engineering"`),
|
||||
"field2": json.RawMessage(`["Go", "React", "Python"]`),
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByEmail(context.TODO(), "testuser@example.com", "").
|
||||
Return(mockUser, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
ListCPAValues(context.TODO(), "user123").
|
||||
Return(mockValues, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := cpaValueListCmdF(s.client, &cobra.Command{}, []string{"testuser@example.com"})
|
||||
s.Require().NoError(err)
|
||||
|
||||
lines := printer.GetLines()
|
||||
s.Require().NotEmpty(lines)
|
||||
})
|
||||
|
||||
s.Run("Should handle empty value list scenario", func() {
|
||||
printer.Clean()
|
||||
printer.SetFormat(printer.FormatPlain)
|
||||
|
||||
mockUser := &model.User{
|
||||
Id: "user123",
|
||||
Username: "testuser",
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByEmail(context.TODO(), "testuser@example.com", "").
|
||||
Return(mockUser, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
ListCPAValues(context.TODO(), "user123").
|
||||
Return(map[string]json.RawMessage{}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := cpaValueListCmdF(s.client, &cobra.Command{}, []string{"testuser@example.com"})
|
||||
s.Require().NoError(err)
|
||||
|
||||
lines := printer.GetLines()
|
||||
// When there are no values, no output should be produced
|
||||
s.Require().Len(lines, 0)
|
||||
})
|
||||
|
||||
s.Run("Should handle API error when ListCPAValues fails", func() {
|
||||
printer.Clean()
|
||||
|
||||
mockUser := &model.User{
|
||||
Id: "user123",
|
||||
Username: "testuser",
|
||||
}
|
||||
|
||||
expectedError := errors.New("API error")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByEmail(context.TODO(), "testuser@example.com", "").
|
||||
Return(mockUser, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
ListCPAValues(context.TODO(), "user123").
|
||||
Return(nil, &model.Response{}, expectedError).
|
||||
Times(1)
|
||||
|
||||
err := cpaValueListCmdF(s.client, &cobra.Command{}, []string{"testuser@example.com"})
|
||||
s.Require().Error(err)
|
||||
s.Require().Contains(err.Error(), "failed to get CPA values for user")
|
||||
s.Require().Contains(err.Error(), "API error")
|
||||
})
|
||||
|
||||
s.Run("Should handle getUserFromArg error", func() {
|
||||
printer.Clean()
|
||||
|
||||
notFoundError := errors.New("user not found")
|
||||
notFoundResponse := &model.Response{StatusCode: http.StatusNotFound}
|
||||
|
||||
// getUserFromArg tries email first, then username, then user ID
|
||||
// All should return NotFoundError so it tries all methods
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByEmail(context.TODO(), "nonexistent@example.com", "").
|
||||
Return(nil, notFoundResponse, notFoundError).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByUsername(context.TODO(), "nonexistent@example.com", "").
|
||||
Return(nil, notFoundResponse, notFoundError).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUser(context.TODO(), "nonexistent@example.com", "").
|
||||
Return(nil, notFoundResponse, notFoundError).
|
||||
Times(1)
|
||||
|
||||
err := cpaValueListCmdF(s.client, &cobra.Command{}, []string{"nonexistent@example.com"})
|
||||
s.Require().Error(err)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestCPAValueSetCmd() {
|
||||
s.Run("Should successfully set single CPA value", func() {
|
||||
printer.Clean()
|
||||
printer.SetFormat(printer.FormatPlain)
|
||||
|
||||
mockUser := &model.User{
|
||||
Id: "user123",
|
||||
Username: "testuser",
|
||||
}
|
||||
|
||||
mockFields := []*model.PropertyField{
|
||||
{
|
||||
ID: "field123",
|
||||
Name: "Department",
|
||||
Type: model.PropertyFieldTypeText,
|
||||
},
|
||||
}
|
||||
|
||||
mockUpdatedValues := map[string]json.RawMessage{
|
||||
"field123": json.RawMessage(`"Engineering"`),
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().StringSlice("value", []string{"Engineering"}, "")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByEmail(context.TODO(), "testuser@example.com", "").
|
||||
Return(mockUser, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
ListCPAFields(context.TODO()).
|
||||
Return(mockFields, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
PatchCPAValuesForUser(context.TODO(), "user123", gomock.Any()).
|
||||
Return(mockUpdatedValues, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := cpaValueSetCmdF(s.client, cmd, []string{"testuser@example.com", "field123"})
|
||||
s.Require().NoError(err)
|
||||
})
|
||||
|
||||
s.Run("Should successfully set multiple CPA values", func() {
|
||||
printer.Clean()
|
||||
printer.SetFormat(printer.FormatPlain)
|
||||
|
||||
mockUser := &model.User{
|
||||
Id: "user123",
|
||||
Username: "testuser",
|
||||
}
|
||||
|
||||
mockFields := []*model.PropertyField{
|
||||
{
|
||||
ID: "field123",
|
||||
Name: "Skills",
|
||||
Type: model.PropertyFieldTypeMultiselect,
|
||||
},
|
||||
}
|
||||
|
||||
mockUpdatedValues := map[string]json.RawMessage{
|
||||
"field123": json.RawMessage(`["Go", "React", "Python"]`),
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().StringSlice("value", []string{"Go", "React", "Python"}, "")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByEmail(context.TODO(), "testuser@example.com", "").
|
||||
Return(mockUser, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
ListCPAFields(context.TODO()).
|
||||
Return(mockFields, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
PatchCPAValuesForUser(context.TODO(), "user123", gomock.Any()).
|
||||
Return(mockUpdatedValues, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := cpaValueSetCmdF(s.client, cmd, []string{"testuser@example.com", "field123"})
|
||||
s.Require().NoError(err)
|
||||
})
|
||||
|
||||
s.Run("Should handle field not found error", func() {
|
||||
printer.Clean()
|
||||
|
||||
mockUser := &model.User{
|
||||
Id: "user123",
|
||||
Username: "testuser",
|
||||
}
|
||||
|
||||
mockFields := []*model.PropertyField{
|
||||
{
|
||||
ID: "different_field",
|
||||
Name: "Department",
|
||||
Type: model.PropertyFieldTypeText,
|
||||
},
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().StringSlice("value", []string{"Engineering"}, "")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByEmail(context.TODO(), "testuser@example.com", "").
|
||||
Return(mockUser, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
ListCPAFields(context.TODO()).
|
||||
Return(mockFields, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := cpaValueSetCmdF(s.client, cmd, []string{"testuser@example.com", "nonexistent_field"})
|
||||
s.Require().Error(err)
|
||||
s.Require().Contains(err.Error(), "field nonexistent_field not found")
|
||||
})
|
||||
|
||||
s.Run("Should handle API error when PatchCPAValuesForUser fails", func() {
|
||||
printer.Clean()
|
||||
|
||||
mockUser := &model.User{
|
||||
Id: "user123",
|
||||
Username: "testuser",
|
||||
}
|
||||
|
||||
mockFields := []*model.PropertyField{
|
||||
{
|
||||
ID: "field123",
|
||||
Name: "Department",
|
||||
Type: model.PropertyFieldTypeText,
|
||||
},
|
||||
}
|
||||
|
||||
expectedError := errors.New("permission denied")
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().StringSlice("value", []string{"Engineering"}, "")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByEmail(context.TODO(), "testuser@example.com", "").
|
||||
Return(mockUser, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
ListCPAFields(context.TODO()).
|
||||
Return(mockFields, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
PatchCPAValuesForUser(context.TODO(), "user123", gomock.Any()).
|
||||
Return(nil, &model.Response{}, expectedError).
|
||||
Times(1)
|
||||
|
||||
err := cpaValueSetCmdF(s.client, cmd, []string{"testuser@example.com", "field123"})
|
||||
s.Require().Error(err)
|
||||
s.Require().Contains(err.Error(), "failed to set CPA value")
|
||||
s.Require().Contains(err.Error(), "permission denied")
|
||||
})
|
||||
|
||||
s.Run("Should handle ListCPAFields API error", func() {
|
||||
printer.Clean()
|
||||
|
||||
mockUser := &model.User{
|
||||
Id: "user123",
|
||||
Username: "testuser",
|
||||
}
|
||||
|
||||
expectedError := errors.New("fields API error")
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().StringSlice("value", []string{"Engineering"}, "")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByEmail(context.TODO(), "testuser@example.com", "").
|
||||
Return(mockUser, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
ListCPAFields(context.TODO()).
|
||||
Return(nil, &model.Response{}, expectedError).
|
||||
Times(1)
|
||||
|
||||
err := cpaValueSetCmdF(s.client, cmd, []string{"testuser@example.com", "field123"})
|
||||
s.Require().Error(err)
|
||||
s.Require().Contains(err.Error(), "failed to get CPA fields")
|
||||
s.Require().Contains(err.Error(), "fields API error")
|
||||
})
|
||||
}
|
||||
@@ -9,7 +9,7 @@ Synopsis
|
||||
~~~~~~~~
|
||||
|
||||
|
||||
Management of Custom Profile Attributes (CPA) fields.
|
||||
Management of Custom Profile Attributes (CPA) fields and values.
|
||||
|
||||
Options
|
||||
~~~~~~~
|
||||
@@ -38,4 +38,5 @@ SEE ALSO
|
||||
|
||||
* `mmctl <mmctl.rst>`_ - Remote client for the Open Source, self-hosted Slack-alternative
|
||||
* `mmctl cpa field <mmctl_cpa_field.rst>`_ - Management of CPA fields
|
||||
* `mmctl cpa value <mmctl_cpa_value.rst>`_ - Management of CPA values
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
.. _mmctl_cpa_value:
|
||||
|
||||
mmctl cpa value
|
||||
---------------
|
||||
|
||||
Management of CPA values
|
||||
|
||||
Synopsis
|
||||
~~~~~~~~
|
||||
|
||||
|
||||
List, set, and delete Custom Profile Attribute values for users.
|
||||
|
||||
Options
|
||||
~~~~~~~
|
||||
|
||||
::
|
||||
|
||||
-h, --help help for value
|
||||
|
||||
Options inherited from parent commands
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
::
|
||||
|
||||
--config string path to the configuration file (default "$XDG_CONFIG_HOME/mmctl/config")
|
||||
--disable-pager disables paged output
|
||||
--insecure-sha1-intermediate allows to use insecure TLS protocols, such as SHA-1
|
||||
--insecure-tls-version allows to use TLS versions 1.0 and 1.1
|
||||
--json the output format will be in json format
|
||||
--local allows communicating with the server through a unix socket
|
||||
--quiet prevent mmctl to generate output for the commands
|
||||
--strict will only run commands if the mmctl version matches the server one
|
||||
--suppress-warnings disables printing warning messages
|
||||
|
||||
SEE ALSO
|
||||
~~~~~~~~
|
||||
|
||||
* `mmctl cpa <mmctl_cpa.rst>`_ - Management of Custom Profile Attributes
|
||||
* `mmctl cpa value list <mmctl_cpa_value_list.rst>`_ - List CPA values for a user
|
||||
* `mmctl cpa value set <mmctl_cpa_value_set.rst>`_ - Set a CPA value for a user
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
.. _mmctl_cpa_value_list:
|
||||
|
||||
mmctl cpa value list
|
||||
--------------------
|
||||
|
||||
List CPA values for a user
|
||||
|
||||
Synopsis
|
||||
~~~~~~~~
|
||||
|
||||
|
||||
List all Custom Profile Attribute values for a specific user.
|
||||
|
||||
::
|
||||
|
||||
mmctl cpa value list [user] [flags]
|
||||
|
||||
Examples
|
||||
~~~~~~~~
|
||||
|
||||
::
|
||||
|
||||
cpa value list john.doe@company.com
|
||||
cpa value list johndoe
|
||||
|
||||
Options
|
||||
~~~~~~~
|
||||
|
||||
::
|
||||
|
||||
-h, --help help for list
|
||||
|
||||
Options inherited from parent commands
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
::
|
||||
|
||||
--config string path to the configuration file (default "$XDG_CONFIG_HOME/mmctl/config")
|
||||
--disable-pager disables paged output
|
||||
--insecure-sha1-intermediate allows to use insecure TLS protocols, such as SHA-1
|
||||
--insecure-tls-version allows to use TLS versions 1.0 and 1.1
|
||||
--json the output format will be in json format
|
||||
--local allows communicating with the server through a unix socket
|
||||
--quiet prevent mmctl to generate output for the commands
|
||||
--strict will only run commands if the mmctl version matches the server one
|
||||
--suppress-warnings disables printing warning messages
|
||||
|
||||
SEE ALSO
|
||||
~~~~~~~~
|
||||
|
||||
* `mmctl cpa value <mmctl_cpa_value.rst>`_ - Management of CPA values
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
.. _mmctl_cpa_value_set:
|
||||
|
||||
mmctl cpa value set
|
||||
-------------------
|
||||
|
||||
Set a CPA value for a user
|
||||
|
||||
Synopsis
|
||||
~~~~~~~~
|
||||
|
||||
|
||||
Set a Custom Profile Attribute field value for a specific user.
|
||||
|
||||
::
|
||||
|
||||
mmctl cpa value set [user] [field-id] [flags]
|
||||
|
||||
Examples
|
||||
~~~~~~~~
|
||||
|
||||
::
|
||||
|
||||
cpa value set john.doe@company.com kx8m2w4r9p3q7n5t1j6h8s4c9e --value "Engineering"
|
||||
cpa value set johndoe q7n3t8w5r2m9k4x6p1j3h7s8c4 --value "Go" --value "React" --value "Python"
|
||||
cpa value set user123 w9r5t2n8k4x7p3q6m1j9h4s7c2 --value "Senior"
|
||||
|
||||
Options
|
||||
~~~~~~~
|
||||
|
||||
::
|
||||
|
||||
-h, --help help for set
|
||||
--value strings Value(s) to set for the field. Can be specified multiple times for multiselect/multiuser fields
|
||||
|
||||
Options inherited from parent commands
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
::
|
||||
|
||||
--config string path to the configuration file (default "$XDG_CONFIG_HOME/mmctl/config")
|
||||
--disable-pager disables paged output
|
||||
--insecure-sha1-intermediate allows to use insecure TLS protocols, such as SHA-1
|
||||
--insecure-tls-version allows to use TLS versions 1.0 and 1.1
|
||||
--json the output format will be in json format
|
||||
--local allows communicating with the server through a unix socket
|
||||
--quiet prevent mmctl to generate output for the commands
|
||||
--strict will only run commands if the mmctl version matches the server one
|
||||
--suppress-warnings disables printing warning messages
|
||||
|
||||
SEE ALSO
|
||||
~~~~~~~~
|
||||
|
||||
* `mmctl cpa value <mmctl_cpa_value.rst>`_ - Management of CPA values
|
||||
|
||||
@@ -1814,6 +1814,22 @@ func (mr *MockClientMockRecorder) PatchCPAValues(arg0, arg1 interface{}) *gomock
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PatchCPAValues", reflect.TypeOf((*MockClient)(nil).PatchCPAValues), arg0, arg1)
|
||||
}
|
||||
|
||||
// PatchCPAValuesForUser mocks base method.
|
||||
func (m *MockClient) PatchCPAValuesForUser(arg0 context.Context, arg1 string, arg2 map[string]json.RawMessage) (map[string]json.RawMessage, *model.Response, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "PatchCPAValuesForUser", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(map[string]json.RawMessage)
|
||||
ret1, _ := ret[1].(*model.Response)
|
||||
ret2, _ := ret[2].(error)
|
||||
return ret0, ret1, ret2
|
||||
}
|
||||
|
||||
// PatchCPAValuesForUser indicates an expected call of PatchCPAValuesForUser.
|
||||
func (mr *MockClientMockRecorder) PatchCPAValuesForUser(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PatchCPAValuesForUser", reflect.TypeOf((*MockClient)(nil).PatchCPAValuesForUser), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// PatchChannel mocks base method.
|
||||
func (m *MockClient) PatchChannel(arg0 context.Context, arg1 string, arg2 *model.ChannelPatch) (*model.Channel, *model.Response, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
@@ -611,7 +611,7 @@ func (c *Client4) customProfileAttributesRoute() string {
|
||||
}
|
||||
|
||||
func (c *Client4) userCustomProfileAttributesRoute(userID string) string {
|
||||
return fmt.Sprintf("%s/%s", c.userRoute(userID), c.customProfileAttributesRoute())
|
||||
return fmt.Sprintf("%s/custom_profile_attributes", c.userRoute(userID))
|
||||
}
|
||||
|
||||
func (c *Client4) customProfileAttributeFieldsRoute() string {
|
||||
@@ -9670,6 +9670,26 @@ func (c *Client4) PatchCPAValues(ctx context.Context, values map[string]json.Raw
|
||||
return patchedValues, BuildResponse(r), nil
|
||||
}
|
||||
|
||||
func (c *Client4) PatchCPAValuesForUser(ctx context.Context, userID string, values map[string]json.RawMessage) (map[string]json.RawMessage, *Response, error) {
|
||||
buf, err := json.Marshal(values)
|
||||
if err != nil {
|
||||
return nil, nil, NewAppError("PatchCPAValuesForUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
r, err := c.DoAPIPatchBytes(ctx, c.userCustomProfileAttributesRoute(userID), buf)
|
||||
if err != nil {
|
||||
return nil, BuildResponse(r), err
|
||||
}
|
||||
defer closeBody(r)
|
||||
|
||||
var patchedValues map[string]json.RawMessage
|
||||
if err := json.NewDecoder(r.Body).Decode(&patchedValues); err != nil {
|
||||
return nil, nil, NewAppError("PatchCPAValuesForUser", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
return patchedValues, BuildResponse(r), nil
|
||||
}
|
||||
|
||||
// Access Control Policies Section
|
||||
|
||||
// CreateAccessControlPolicy creates a new access control policy.
|
||||
|
||||
Reference in New Issue
Block a user