mirror of
https://github.com/hanzoai/team.git
synced 2026-09-30 01:57:35 +00:00
* Refactor property system with app layer routing and access control separation Establish the app layer as the primary entry point for property operations with intelligent routing based on group type. This architecture separates access-controlled operations (CPA groups) from standard operations, improving performance and code clarity. Architecture Changes: - App layer now routes operations based on group type: - CPA groups -> PropertyAccessService (enforces access control) - Non-CPA groups -> PropertyService (direct, no access control) - PropertyAccessService simplified to handle only CPA operations - Eliminated redundant group type checks throughout the codebase * Move access control routing into PropertyService This change makes the PropertyService the main entrypoint for property related operations, and adds a routing mechanism to decide if extra behaviors or checks should run for each operation, in this case, the property access service logic. To add specific payloads that pluggable checks and operations may need, we use the request context. When the request comes from the API, the endpoints are in charge of adding the caller ID to the payload, and in the case of the plugin API, on receiving a request, the server automatically tags the context with the plugin ID so the property service can react accordingly. Finally, the new design enforces all these checks migrating the actual property logic to internal, non-exposed methods, so any caller from the App layer needs to go through the service checks that decide if pluggable logic is needed, avoiding any possibility of a bypass. * Fix i18n * Fix bad error string * Added nil guards to property methods * Add check for multiple group IDs on value operations * Add nil guard to the plugin checker * Fix build error * Update value tests * Fix linter * Adds early return when content flaggin a thread with no replies * Fix mocks * Clean the state of plugin property tests before each run * Do not wrap appErr on API response and fix i18n * Fix create property field test * Remove the need to cache cpaGroupID as part of the property service * Split the property.go file into multiple * Not found group doesn't bypass access control check * Unexport SetPluginCheckerForTests * Rename plugin context getter to be more PSA specific --------- Co-authored-by: Miguel de la Cruz <[email protected]>
373 lines
11 KiB
Go
373 lines
11 KiB
Go
// 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(nil, 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(nil, userID, updates, false)
|
|
s.Require().Nil(appErr)
|
|
}
|
|
}
|
|
|
|
func (s *MmctlE2ETestSuite) TestCPAValueList() {
|
|
s.SetupEnterpriseTestHelper().InitBasic(s.T())
|
|
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(nil, 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(nil, s.th.BasicUser.Id, updates, false)
|
|
s.Require().Nil(appErr)
|
|
|
|
// Test listing the values with plain format (human-readable)
|
|
printer.SetFormat(printer.FormatPlain)
|
|
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 human-readable format is used
|
|
output := printer.GetLines()[0].(string)
|
|
s.Require().Equal("Department (text): Engineering", output)
|
|
|
|
// Test with JSON format to ensure raw data is preserved
|
|
printer.Clean()
|
|
printer.SetFormat(printer.FormatJSON)
|
|
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 JSON format outputs raw data structure
|
|
outputMap := printer.GetLines()[0].(map[string]any)
|
|
s.Require().Contains(outputMap, createdField.ID)
|
|
s.Require().Equal(`"Engineering"`, string(outputMap[createdField.ID].(json.RawMessage)))
|
|
})
|
|
}
|
|
|
|
func (s *MmctlE2ETestSuite) TestCPAValueSet() {
|
|
s.SetupEnterpriseTestHelper().InitBasic(s.T())
|
|
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(nil, 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(nil, 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(nil, selectField)
|
|
s.Require().Nil(appErr)
|
|
|
|
// 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(nil, 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 createdField.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(nil, multiselectField)
|
|
s.Require().Nil(appErr)
|
|
|
|
// 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(nil, 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 createdField.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.Run("Set a single 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: "Programming Languages",
|
|
Type: model.PropertyFieldTypeMultiselect,
|
|
TargetType: "user",
|
|
},
|
|
Attrs: model.CPAAttrs{
|
|
Managed: "",
|
|
Options: []*model.CustomProfileAttributesSelectOption{
|
|
{ID: model.NewId(), Name: "Go"},
|
|
{ID: model.NewId(), Name: "Python"},
|
|
{ID: model.NewId(), Name: "JavaScript"},
|
|
},
|
|
},
|
|
}
|
|
|
|
createdField, appErr := s.th.App.CreateCPAField(nil, multiselectField)
|
|
s.Require().Nil(appErr)
|
|
|
|
// Set a single value using option name
|
|
cmd := &cobra.Command{}
|
|
cmd.Flags().StringSlice("value", []string{}, "")
|
|
|
|
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 value was set (should be stored as an array with single option ID)
|
|
|
|
values, appErr := s.th.App.ListCPAValues(nil, s.th.BasicUser.Id)
|
|
s.Require().Nil(appErr)
|
|
s.Require().Len(values, 1)
|
|
s.Require().Equal(createdField.ID, values[0].FieldID)
|
|
|
|
// Find the option ID for verification
|
|
var pythonOptionID string
|
|
for _, option := range createdField.Attrs.Options {
|
|
if option.Name == "Python" {
|
|
pythonOptionID = option.ID
|
|
break
|
|
}
|
|
}
|
|
|
|
// The multiselect value should be stored as an array with single option ID
|
|
// Even for single value, multiselect fields store values as arrays
|
|
actualValue := string(values[0].Value)
|
|
s.Require().Contains(actualValue, pythonOptionID)
|
|
s.Require().Contains(actualValue, "[")
|
|
s.Require().Contains(actualValue, "]")
|
|
// Verify it doesn't contain other option IDs
|
|
for _, option := range createdField.Attrs.Options {
|
|
if option.Name != "Python" {
|
|
s.Require().NotContains(actualValue, option.ID)
|
|
}
|
|
}
|
|
})
|
|
|
|
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(nil, 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(nil, 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))
|
|
})
|
|
}
|