mirror of
https://github.com/hanzoai/team.git
synced 2026-09-30 01:57:35 +00:00
The server module named github.com/hanzoai/team/server/v8 — a path that resolves to the retired workspace's repository, not this one — while server/public still published github.com/mattermost/mattermost/server/public. Both are github.com/hanzoteam/server/... now, which is where this code actually lives. That second half is what made the binary unbootable. Upstream's public path stayed claimed by upstream, mattermost-plugin-ai imported it, and the build linked public/model twice; public/plugin's gob.Register then ran twice under one name and panicked at init, before main. Reverting the rename traded the brand for a working binary. Rebranding the plugin alongside it needs no such trade: the agents fork is github.com/hanzoteam/agents/v2 against our public module, so no module named mattermost/mattermost is in the graph at all. Measured: `go list -deps ./cmd/mattermost` names exactly one public/model, and it is ours.
291 lines
9.8 KiB
Go
291 lines
9.8 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package app
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
"github.com/hanzoteam/server/server/public/shared/request"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestGetCPAValue(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
|
|
|
|
cpaGroup, groupErr := th.App.GetPropertyGroup(request.TestContext(t), model.AccessControlPropertyGroupName)
|
|
require.Nil(t, groupErr)
|
|
cpaID := cpaGroup.ID
|
|
|
|
rctx := th.emptyContextWithCallerID(anonymousCallerId)
|
|
|
|
field := &model.PropertyField{
|
|
GroupID: cpaID,
|
|
Name: "f_" + model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
}
|
|
createdField, err := th.App.CreatePropertyField(rctx, field, false, "")
|
|
require.Nil(t, err)
|
|
fieldID := createdField.ID
|
|
|
|
t.Run("should fail if the value doesn't exist", func(t *testing.T) {
|
|
pv, appErr := th.App.GetPropertyValue(rctx, cpaID, model.NewId())
|
|
require.NotNil(t, appErr)
|
|
require.Nil(t, pv)
|
|
})
|
|
|
|
t.Run("should fail if the group id is invalid", func(t *testing.T) {
|
|
propertyValue := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: model.PropertyValueTargetTypeUser,
|
|
GroupID: model.NewId(),
|
|
FieldID: fieldID,
|
|
Value: json.RawMessage(`"Value"`),
|
|
}
|
|
created, err := th.App.CreatePropertyValue(rctx, propertyValue)
|
|
require.Nil(t, err)
|
|
require.NotNil(t, created)
|
|
|
|
pv, appErr := th.App.GetPropertyValue(rctx, cpaID, created.ID)
|
|
require.NotNil(t, appErr)
|
|
require.Nil(t, pv)
|
|
})
|
|
|
|
t.Run("should succeed if id exists", func(t *testing.T) {
|
|
propertyValue := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: model.PropertyValueTargetTypeUser,
|
|
GroupID: cpaID,
|
|
FieldID: fieldID,
|
|
Value: json.RawMessage(`"Value"`),
|
|
}
|
|
propertyValue, err := th.App.CreatePropertyValue(rctx, propertyValue)
|
|
require.Nil(t, err)
|
|
|
|
pv, appErr := th.App.GetPropertyValue(rctx, cpaID, propertyValue.ID)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, pv)
|
|
})
|
|
|
|
t.Run("should handle array values correctly", func(t *testing.T) {
|
|
optionIDs := []string{model.NewId(), model.NewId(), model.NewId()}
|
|
arrayField := &model.PropertyField{
|
|
GroupID: cpaID,
|
|
Name: "f_" + model.NewId(),
|
|
Type: model.PropertyFieldTypeMultiselect,
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
Attrs: model.StringInterface{
|
|
model.PropertyFieldAttributeOptions: []any{
|
|
map[string]any{"id": optionIDs[0], "name": "option1"},
|
|
map[string]any{"id": optionIDs[1], "name": "option2"},
|
|
map[string]any{"id": optionIDs[2], "name": "option3"},
|
|
},
|
|
},
|
|
}
|
|
createdField, err := th.App.CreatePropertyField(rctx, arrayField, false, "")
|
|
require.Nil(t, err)
|
|
|
|
propertyValue := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: model.PropertyValueTargetTypeUser,
|
|
GroupID: cpaID,
|
|
FieldID: createdField.ID,
|
|
Value: json.RawMessage(fmt.Sprintf(`["%s", "%s", "%s"]`, optionIDs[0], optionIDs[1], optionIDs[2])),
|
|
}
|
|
propertyValue, err = th.App.CreatePropertyValue(rctx, propertyValue)
|
|
require.Nil(t, err)
|
|
|
|
pv, appErr := th.App.GetPropertyValue(rctx, cpaID, propertyValue.ID)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, pv)
|
|
var arrayValues []string
|
|
require.NoError(t, json.Unmarshal(pv.Value, &arrayValues))
|
|
require.Equal(t, optionIDs, arrayValues)
|
|
})
|
|
}
|
|
|
|
func TestDeleteCPAValues(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
|
|
|
|
cpaGroup, groupErr := th.App.GetPropertyGroup(request.TestContext(t), model.AccessControlPropertyGroupName)
|
|
require.Nil(t, groupErr)
|
|
cpaID := cpaGroup.ID
|
|
|
|
rctx := th.emptyContextWithCallerID(anonymousCallerId)
|
|
|
|
userID := model.NewId()
|
|
otherUserID := model.NewId()
|
|
|
|
listValues := func(targetID string) []*model.PropertyValue {
|
|
t.Helper()
|
|
values, appErr := th.App.SearchPropertyValues(rctx, cpaID, model.PropertyValueSearchOpts{
|
|
TargetIDs: []string{targetID},
|
|
TargetType: model.PropertyValueTargetTypeUser,
|
|
// Single-target search: at most one value per (target, field), so the field cap bounds the page.
|
|
PerPage: model.AccessControlGroupFieldLimit + 5,
|
|
})
|
|
require.Nil(t, appErr)
|
|
return values
|
|
}
|
|
|
|
// Create multiple fields and a value per field for userID.
|
|
var createdFields []*model.PropertyField
|
|
for i := 1; i <= 3; i++ {
|
|
field := &model.PropertyField{
|
|
GroupID: cpaID,
|
|
Name: fmt.Sprintf("field_%d", i),
|
|
Type: model.PropertyFieldTypeText,
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
}
|
|
createdField, err := th.App.CreatePropertyField(rctx, field, false, "")
|
|
require.Nil(t, err)
|
|
createdFields = append(createdFields, createdField)
|
|
|
|
value := &model.PropertyValue{
|
|
TargetID: userID,
|
|
TargetType: model.PropertyValueTargetTypeUser,
|
|
GroupID: cpaID,
|
|
FieldID: createdField.ID,
|
|
Value: json.RawMessage(fmt.Sprintf(`"Value %d"`, i)),
|
|
}
|
|
_, err = th.App.CreatePropertyValue(rctx, value)
|
|
require.Nil(t, err)
|
|
}
|
|
|
|
require.Len(t, listValues(userID), 3)
|
|
|
|
t.Run("should delete all values for a user", func(t *testing.T) {
|
|
appErr := th.App.DeletePropertyValuesForTarget(rctx, cpaID, model.PropertyFieldObjectTypeUser, userID)
|
|
require.Nil(t, appErr)
|
|
|
|
require.Empty(t, listValues(userID))
|
|
})
|
|
|
|
t.Run("should handle deleting values for a user with no values", func(t *testing.T) {
|
|
appErr := th.App.DeletePropertyValuesForTarget(rctx, cpaID, model.PropertyFieldObjectTypeUser, otherUserID)
|
|
require.Nil(t, appErr)
|
|
})
|
|
|
|
t.Run("should not affect values for other users", func(t *testing.T) {
|
|
// Create values for otherUserID.
|
|
for _, field := range createdFields {
|
|
value := &model.PropertyValue{
|
|
TargetID: otherUserID,
|
|
TargetType: model.PropertyValueTargetTypeUser,
|
|
GroupID: cpaID,
|
|
FieldID: field.ID,
|
|
Value: json.RawMessage(`"Other user value"`),
|
|
}
|
|
_, err := th.App.CreatePropertyValue(rctx, value)
|
|
require.Nil(t, err)
|
|
}
|
|
|
|
appErr := th.App.DeletePropertyValuesForTarget(rctx, cpaID, model.PropertyFieldObjectTypeUser, userID)
|
|
require.Nil(t, appErr)
|
|
|
|
require.Len(t, listValues(otherUserID), 3)
|
|
})
|
|
}
|
|
|
|
// TestCPAValueSyncLock exercises AccessControlHook.checkSyncLock end-to-end
|
|
// at the app layer: a write for a field with ldap= or saml= set only
|
|
// succeeds when the caller ID matches the field's sync source. Covering this
|
|
// at the app layer also asserts that the startup wiring in server.go
|
|
// (access_control group registration, AccessControlHook install, and
|
|
// CallerIDExtractor reading from request.CTX) is intact — something the
|
|
// properties-package tests cannot verify because they install the hook
|
|
// themselves.
|
|
func TestCPAValueSyncLock(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
|
|
|
|
cpaGroup, groupErr := th.App.GetPropertyGroup(request.TestContext(t), model.AccessControlPropertyGroupName)
|
|
require.Nil(t, groupErr)
|
|
cpaID := cpaGroup.ID
|
|
|
|
adminRctx := th.emptyContextWithCallerID(th.SystemAdminUser.Id)
|
|
|
|
createField := func(name string, attrs model.CPAAttrs) *model.PropertyField {
|
|
t.Helper()
|
|
cpa := &model.CPAField{
|
|
PropertyField: model.PropertyField{
|
|
GroupID: cpaID,
|
|
Name: name,
|
|
Type: model.PropertyFieldTypeText,
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
},
|
|
Attrs: attrs,
|
|
}
|
|
// Sanitization/validation runs inside CreatePropertyField via the
|
|
// AccessControlAttributeValidationHook.
|
|
created, appErr := th.App.CreatePropertyField(adminRctx, cpa.ToPropertyField(), false, "")
|
|
require.Nil(t, appErr)
|
|
return created
|
|
}
|
|
|
|
ldapField := createField("ldap_attr_"+model.NewId(), model.CPAAttrs{LDAP: "mail"})
|
|
samlField := createField("saml_attr_"+model.NewId(), model.CPAAttrs{SAML: "email"})
|
|
plainField := createField("plain_attr_"+model.NewId(), model.CPAAttrs{})
|
|
|
|
userID := model.NewId()
|
|
upsertAs := func(callerID string, field *model.PropertyField) *model.AppError {
|
|
t.Helper()
|
|
rctx := th.emptyContextWithCallerID(callerID)
|
|
_, appErr := th.App.UpsertPropertyValues(rctx, []*model.PropertyValue{{
|
|
GroupID: cpaID,
|
|
TargetType: model.PropertyValueTargetTypeUser,
|
|
TargetID: userID,
|
|
FieldID: field.ID,
|
|
Value: json.RawMessage(`"value"`),
|
|
}}, model.PropertyFieldObjectTypeUser, userID, "")
|
|
return appErr
|
|
}
|
|
|
|
requireSyncLock := func(appErr *model.AppError) {
|
|
t.Helper()
|
|
require.NotNil(t, appErr)
|
|
require.Equal(t, "app.property.sync_lock.app_error", appErr.Id)
|
|
}
|
|
|
|
t.Run("anonymous caller is blocked on an LDAP-synced field", func(t *testing.T) {
|
|
requireSyncLock(upsertAs(anonymousCallerId, ldapField))
|
|
})
|
|
|
|
t.Run("anonymous caller is blocked on a SAML-synced field", func(t *testing.T) {
|
|
requireSyncLock(upsertAs(anonymousCallerId, samlField))
|
|
})
|
|
|
|
t.Run("anonymous caller is allowed on a non-synced field", func(t *testing.T) {
|
|
require.Nil(t, upsertAs(anonymousCallerId, plainField))
|
|
})
|
|
|
|
t.Run("LDAP sync caller is allowed on an LDAP-synced field", func(t *testing.T) {
|
|
require.Nil(t, upsertAs(model.CallerIDLDAPSync, ldapField))
|
|
})
|
|
|
|
t.Run("LDAP sync caller is blocked on a SAML-synced field", func(t *testing.T) {
|
|
requireSyncLock(upsertAs(model.CallerIDLDAPSync, samlField))
|
|
})
|
|
|
|
t.Run("SAML sync caller is allowed on a SAML-synced field", func(t *testing.T) {
|
|
require.Nil(t, upsertAs(model.CallerIDSAMLSync, samlField))
|
|
})
|
|
|
|
t.Run("SAML sync caller is blocked on an LDAP-synced field", func(t *testing.T) {
|
|
requireSyncLock(upsertAs(model.CallerIDSAMLSync, ldapField))
|
|
})
|
|
}
|