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.
1509 lines
57 KiB
Go
1509 lines
57 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package properties
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"testing"
|
|
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestGetPropertyFieldReadAccess(t *testing.T) {
|
|
th := Setup(t).RegisterCPAPropertyGroup(t)
|
|
// only "plugin-1" and "test-plugin" will be checked as installed
|
|
th.service.setPluginCheckerForTests(func(pluginID string) bool {
|
|
return pluginID == "plugin-1" || pluginID == "test-plugin"
|
|
})
|
|
|
|
pluginID1 := "plugin-1"
|
|
pluginID2 := "plugin-2"
|
|
userID := model.NewId()
|
|
|
|
rctxAnon := RequestContextWithCallerID(th.Context, "")
|
|
rctx1 := RequestContextWithCallerID(th.Context, pluginID1)
|
|
rctx2 := RequestContextWithCallerID(th.Context, pluginID2)
|
|
rctxUser := RequestContextWithCallerID(th.Context, userID)
|
|
rctxTestPlugin := RequestContextWithCallerID(th.Context, "test-plugin")
|
|
|
|
t.Run("public field - any caller can read without filtering", func(t *testing.T) {
|
|
field := &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
Name: "public-field",
|
|
Type: model.PropertyFieldTypeSelect,
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
Attrs: model.StringInterface{
|
|
model.PropertyAttrsAccessMode: model.PropertyAccessModePublic,
|
|
model.PropertyFieldAttributeOptions: []any{
|
|
map[string]any{"id": "opt1", "value": "Option 1"},
|
|
map[string]any{"id": "opt2", "value": "Option 2"},
|
|
},
|
|
},
|
|
}
|
|
created, err := th.service.CreatePropertyField(rctxAnon, field)
|
|
require.NoError(t, err)
|
|
|
|
// Plugin 1 can read
|
|
retrieved, err := th.service.GetPropertyField(rctx1, th.CPAGroupID, created.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, created.ID, retrieved.ID)
|
|
assert.Len(t, retrieved.Attrs[model.PropertyFieldAttributeOptions].([]any), 2)
|
|
|
|
// Plugin 2 can read
|
|
retrieved, err = th.service.GetPropertyField(rctx2, th.CPAGroupID, created.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, created.ID, retrieved.ID)
|
|
assert.Len(t, retrieved.Attrs[model.PropertyFieldAttributeOptions].([]any), 2)
|
|
|
|
// User can read
|
|
retrieved, err = th.service.GetPropertyField(rctxUser, th.CPAGroupID, created.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, created.ID, retrieved.ID)
|
|
assert.Len(t, retrieved.Attrs[model.PropertyFieldAttributeOptions].([]any), 2)
|
|
|
|
// Anonymous caller can read
|
|
retrieved, err = th.service.GetPropertyField(rctxAnon, th.CPAGroupID, created.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, created.ID, retrieved.ID)
|
|
assert.Len(t, retrieved.Attrs[model.PropertyFieldAttributeOptions].([]any), 2)
|
|
})
|
|
|
|
t.Run("source_only field - source plugin gets all options", func(t *testing.T) {
|
|
field := &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
Name: "source-only-field",
|
|
Type: model.PropertyFieldTypeSelect,
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
Attrs: model.StringInterface{
|
|
model.PropertyAttrsAccessMode: model.PropertyAccessModeSourceOnly,
|
|
model.PropertyAttrsProtected: true,
|
|
model.PropertyFieldAttributeOptions: []any{
|
|
map[string]any{"id": "opt1", "value": "Secret Option 1"},
|
|
map[string]any{"id": "opt2", "value": "Secret Option 2"},
|
|
},
|
|
},
|
|
}
|
|
created, err := th.service.CreatePropertyField(rctx1, field)
|
|
require.NoError(t, err)
|
|
|
|
// Source plugin can see all options
|
|
retrieved, err := th.service.GetPropertyField(rctx1, th.CPAGroupID, created.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, created.ID, retrieved.ID)
|
|
assert.Len(t, retrieved.Attrs[model.PropertyFieldAttributeOptions].([]any), 2)
|
|
})
|
|
|
|
t.Run("source_only field - other plugin gets empty options", func(t *testing.T) {
|
|
field := &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
Name: "source-only-field-2",
|
|
Type: model.PropertyFieldTypeSelect,
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
Attrs: model.StringInterface{
|
|
model.PropertyAttrsAccessMode: model.PropertyAccessModeSourceOnly,
|
|
model.PropertyAttrsProtected: true,
|
|
model.PropertyFieldAttributeOptions: []any{
|
|
map[string]any{"id": "opt1", "value": "Secret Option 1"},
|
|
map[string]any{"id": "opt2", "value": "Secret Option 2"},
|
|
},
|
|
},
|
|
}
|
|
created, err := th.service.CreatePropertyField(rctx1, field)
|
|
require.NoError(t, err)
|
|
|
|
// Other plugin gets field but with empty options
|
|
retrieved, err := th.service.GetPropertyField(rctx2, th.CPAGroupID, created.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, created.ID, retrieved.ID)
|
|
assert.Empty(t, retrieved.Attrs[model.PropertyFieldAttributeOptions].([]any))
|
|
})
|
|
|
|
t.Run("source_only field - user gets empty options", func(t *testing.T) {
|
|
field := &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
Name: "source-only-field-3",
|
|
Type: model.PropertyFieldTypeSelect,
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
Attrs: model.StringInterface{
|
|
model.PropertyAttrsAccessMode: model.PropertyAccessModeSourceOnly,
|
|
model.PropertyAttrsProtected: true,
|
|
model.PropertyFieldAttributeOptions: []any{
|
|
map[string]any{"id": "opt1", "value": "Secret Option 1"},
|
|
map[string]any{"id": "opt2", "value": "Secret Option 2"},
|
|
},
|
|
},
|
|
}
|
|
created, err := th.service.CreatePropertyField(rctx1, field)
|
|
require.NoError(t, err)
|
|
|
|
// User gets field but with empty options
|
|
retrieved, err := th.service.GetPropertyField(rctxUser, th.CPAGroupID, created.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, created.ID, retrieved.ID)
|
|
assert.Empty(t, retrieved.Attrs[model.PropertyFieldAttributeOptions].([]any))
|
|
})
|
|
|
|
t.Run("source_only field - anonymous caller gets empty options", func(t *testing.T) {
|
|
field := &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
Name: "source-only-field-4",
|
|
Type: model.PropertyFieldTypeSelect,
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
Attrs: model.StringInterface{
|
|
model.PropertyAttrsAccessMode: model.PropertyAccessModeSourceOnly,
|
|
model.PropertyAttrsProtected: true,
|
|
model.PropertyFieldAttributeOptions: []any{
|
|
map[string]any{"id": "opt1", "value": "Secret Option 1"},
|
|
map[string]any{"id": "opt2", "value": "Secret Option 2"},
|
|
},
|
|
},
|
|
}
|
|
created, err := th.service.CreatePropertyField(rctx1, field)
|
|
require.NoError(t, err)
|
|
|
|
// Anonymous caller gets field but with empty options
|
|
retrieved, err := th.service.GetPropertyField(rctxAnon, th.CPAGroupID, created.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, created.ID, retrieved.ID)
|
|
assert.Empty(t, retrieved.Attrs[model.PropertyFieldAttributeOptions].([]any))
|
|
})
|
|
|
|
t.Run("shared_only field - caller with values sees filtered options", func(t *testing.T) {
|
|
field := &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
Name: "shared-only-field",
|
|
Type: model.PropertyFieldTypeMultiselect,
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
Attrs: model.StringInterface{
|
|
model.PropertyAttrsAccessMode: model.PropertyAccessModeSharedOnly,
|
|
model.PropertyAttrsProtected: true,
|
|
model.PropertyFieldAttributeOptions: []any{
|
|
map[string]any{"id": "opt1", "value": "Option 1"},
|
|
map[string]any{"id": "opt2", "value": "Option 2"},
|
|
map[string]any{"id": "opt3", "value": "Option 3"},
|
|
},
|
|
},
|
|
}
|
|
created, err := th.service.CreatePropertyField(rctxTestPlugin, field)
|
|
require.NoError(t, err)
|
|
|
|
// Create values for the caller (userID has opt1 and opt2)
|
|
value1, jsonErr := json.Marshal([]string{"opt1", "opt2"})
|
|
require.NoError(t, jsonErr)
|
|
_, err = th.service.CreatePropertyValue(rctxTestPlugin, &model.PropertyValue{
|
|
GroupID: th.CPAGroupID,
|
|
FieldID: created.ID,
|
|
TargetType: "user",
|
|
TargetID: userID,
|
|
Value: value1,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// User should only see opt1 and opt2
|
|
retrieved, err := th.service.GetPropertyField(rctxUser, th.CPAGroupID, created.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, created.ID, retrieved.ID)
|
|
options := retrieved.Attrs[model.PropertyFieldAttributeOptions].([]any)
|
|
assert.Len(t, options, 2)
|
|
|
|
// Verify the options are opt1 and opt2
|
|
optionIDs := make([]string, 0, len(options))
|
|
for _, opt := range options {
|
|
optMap := opt.(map[string]any)
|
|
optionIDs = append(optionIDs, optMap["id"].(string))
|
|
}
|
|
assert.Contains(t, optionIDs, "opt1")
|
|
assert.Contains(t, optionIDs, "opt2")
|
|
assert.NotContains(t, optionIDs, "opt3")
|
|
})
|
|
|
|
t.Run("shared_only field - caller with no values sees empty options", func(t *testing.T) {
|
|
field := &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
Name: "shared-only-field-2",
|
|
Type: model.PropertyFieldTypeMultiselect,
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
Attrs: model.StringInterface{
|
|
model.PropertyAttrsAccessMode: model.PropertyAccessModeSharedOnly,
|
|
model.PropertyAttrsProtected: true,
|
|
model.PropertyFieldAttributeOptions: []any{
|
|
map[string]any{"id": "opt1", "value": "Option 1"},
|
|
map[string]any{"id": "opt2", "value": "Option 2"},
|
|
},
|
|
},
|
|
}
|
|
created, err := th.service.CreatePropertyField(rctxTestPlugin, field)
|
|
require.NoError(t, err)
|
|
|
|
// User has no values for this field
|
|
retrieved, err := th.service.GetPropertyField(rctxUser, th.CPAGroupID, created.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, created.ID, retrieved.ID)
|
|
assert.Empty(t, retrieved.Attrs[model.PropertyFieldAttributeOptions].([]any))
|
|
})
|
|
|
|
t.Run("shared_only field - source plugin gets all options", func(t *testing.T) {
|
|
field := &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
Name: "shared-only-field-source",
|
|
Type: model.PropertyFieldTypeMultiselect,
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
Attrs: model.StringInterface{
|
|
model.PropertyAttrsAccessMode: model.PropertyAccessModeSharedOnly,
|
|
model.PropertyAttrsSourcePluginID: pluginID1,
|
|
model.PropertyAttrsProtected: true,
|
|
model.PropertyFieldAttributeOptions: []any{
|
|
map[string]any{"id": "opt1", "value": "Option 1"},
|
|
map[string]any{"id": "opt2", "value": "Option 2"},
|
|
map[string]any{"id": "opt3", "value": "Option 3"},
|
|
},
|
|
},
|
|
}
|
|
created, err := th.service.CreatePropertyField(rctx1, field)
|
|
require.NoError(t, err)
|
|
|
|
// Source plugin can see all options even without having any values
|
|
retrieved, err := th.service.GetPropertyField(rctx1, th.CPAGroupID, created.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, created.ID, retrieved.ID)
|
|
options := retrieved.Attrs[model.PropertyFieldAttributeOptions].([]any)
|
|
assert.Len(t, options, 3)
|
|
|
|
// Other plugin with no values sees empty options
|
|
retrieved, err = th.service.GetPropertyField(rctx2, th.CPAGroupID, created.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, created.ID, retrieved.ID)
|
|
assert.Empty(t, retrieved.Attrs[model.PropertyFieldAttributeOptions].([]any))
|
|
})
|
|
|
|
t.Run("non-CPA group routes directly to PropertyService without filtering", func(t *testing.T) {
|
|
nonCpaGroup, err := th.service.RegisterPropertyGroup(&model.PropertyGroup{Name: "other_group_routing_read", Version: model.PropertyGroupVersionV2})
|
|
require.NoError(t, err)
|
|
|
|
field := &model.PropertyField{
|
|
GroupID: nonCpaGroup.ID,
|
|
Name: "routing-test-non-cpa-source-only",
|
|
Type: model.PropertyFieldTypeSelect,
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
Attrs: model.StringInterface{
|
|
model.PropertyAttrsAccessMode: model.PropertyAccessModeSourceOnly,
|
|
model.PropertyAttrsProtected: true,
|
|
model.PropertyAttrsSourcePluginID: pluginID1,
|
|
model.PropertyFieldAttributeOptions: []any{
|
|
map[string]any{"id": "opt1", "value": "Option 1"},
|
|
map[string]any{"id": "opt2", "value": "Option 2"},
|
|
},
|
|
},
|
|
}
|
|
rctx1 := RequestContextWithCallerID(th.Context, pluginID1)
|
|
created, err := th.service.CreatePropertyField(rctx1, field)
|
|
require.NoError(t, err)
|
|
|
|
// Other plugin sees ALL options (no filtering, goes directly to PropertyService)
|
|
rctx2 := RequestContextWithCallerID(th.Context, "plugin-2")
|
|
retrieved, err := th.service.GetPropertyField(rctx2, nonCpaGroup.ID, created.ID)
|
|
require.NoError(t, err)
|
|
assert.Len(t, retrieved.Attrs[model.PropertyFieldAttributeOptions].([]any), 2)
|
|
})
|
|
|
|
t.Run("field with no attrs defaults to public", func(t *testing.T) {
|
|
field := &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
Name: "no-attrs-field",
|
|
Type: model.PropertyFieldTypeText,
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
Attrs: nil,
|
|
}
|
|
created, err := th.service.CreatePropertyField(rctxAnon, field)
|
|
require.NoError(t, err)
|
|
|
|
// Any caller can read
|
|
retrieved, err := th.service.GetPropertyField(rctxUser, th.CPAGroupID, created.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, created.ID, retrieved.ID)
|
|
})
|
|
|
|
t.Run("field with empty access_mode defaults to public", func(t *testing.T) {
|
|
field := &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
Name: "empty-access-mode-field",
|
|
Type: model.PropertyFieldTypeText,
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
Attrs: model.StringInterface{},
|
|
}
|
|
created, err := th.service.CreatePropertyField(rctxAnon, field)
|
|
require.NoError(t, err)
|
|
|
|
// Any caller can read
|
|
retrieved, err := th.service.GetPropertyField(rctxUser, th.CPAGroupID, created.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, created.ID, retrieved.ID)
|
|
})
|
|
|
|
t.Run("field with invalid access_mode is rejected", func(t *testing.T) {
|
|
field := &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
Name: "invalid-access-mode-field",
|
|
Type: model.PropertyFieldTypeSelect,
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
Attrs: model.StringInterface{
|
|
model.PropertyAttrsAccessMode: "invalid-mode",
|
|
model.PropertyFieldAttributeOptions: []any{
|
|
map[string]any{"id": "opt1", "value": "Option 1"},
|
|
},
|
|
},
|
|
}
|
|
_, err := th.service.CreatePropertyField(rctxAnon, field)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "invalid access mode")
|
|
})
|
|
}
|
|
|
|
func TestGetPropertyFieldsReadAccess(t *testing.T) {
|
|
th := Setup(t).RegisterCPAPropertyGroup(t)
|
|
th.service.setPluginCheckerForTests(func(pluginID string) bool {
|
|
return pluginID == "plugin-1" || pluginID == "test-plugin"
|
|
})
|
|
|
|
pluginID := "plugin-1"
|
|
userID := model.NewId()
|
|
|
|
rctxAnon := RequestContextWithCallerID(th.Context, "")
|
|
rctxPlugin := RequestContextWithCallerID(th.Context, pluginID)
|
|
rctxUser := RequestContextWithCallerID(th.Context, userID)
|
|
rctxTestPlugin := RequestContextWithCallerID(th.Context, "test-plugin")
|
|
|
|
// Create multiple fields with different access modes
|
|
publicField := &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
Name: "public-field",
|
|
Type: model.PropertyFieldTypeText,
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
Attrs: model.StringInterface{
|
|
model.PropertyAttrsAccessMode: model.PropertyAccessModePublic,
|
|
},
|
|
}
|
|
publicField, err := th.service.CreatePropertyField(rctxAnon, publicField)
|
|
require.NoError(t, err)
|
|
|
|
sourceOnlyField := &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
Name: "source-only-field",
|
|
Type: model.PropertyFieldTypeSelect,
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
Attrs: model.StringInterface{
|
|
model.PropertyAttrsAccessMode: model.PropertyAccessModeSourceOnly,
|
|
model.PropertyAttrsProtected: true,
|
|
model.PropertyFieldAttributeOptions: []any{
|
|
map[string]any{"id": "opt1", "value": "Secret Option"},
|
|
},
|
|
},
|
|
}
|
|
sourceOnlyField, err = th.service.CreatePropertyField(rctxPlugin, sourceOnlyField)
|
|
require.NoError(t, err)
|
|
|
|
sharedOnlyField := &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
Name: "shared-only-field",
|
|
Type: model.PropertyFieldTypeMultiselect,
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
Attrs: model.StringInterface{
|
|
model.PropertyAttrsAccessMode: model.PropertyAccessModeSharedOnly,
|
|
model.PropertyAttrsProtected: true,
|
|
model.PropertyFieldAttributeOptions: []any{
|
|
map[string]any{"id": "opt1", "value": "Option 1"},
|
|
map[string]any{"id": "opt2", "value": "Option 2"},
|
|
},
|
|
},
|
|
}
|
|
sharedOnlyField, err = th.service.CreatePropertyField(rctxTestPlugin, sharedOnlyField)
|
|
require.NoError(t, err)
|
|
|
|
// Create a value for userID on the shared field (opt1)
|
|
value, jsonErr := json.Marshal([]string{"opt1"})
|
|
require.NoError(t, jsonErr)
|
|
_, err = th.service.CreatePropertyValue(rctxTestPlugin, &model.PropertyValue{
|
|
GroupID: th.CPAGroupID,
|
|
FieldID: sharedOnlyField.ID,
|
|
TargetType: "user",
|
|
TargetID: userID,
|
|
Value: value,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
t.Run("source plugin sees all fields with full options", func(t *testing.T) {
|
|
fields, err := th.service.GetPropertyFields(rctxPlugin, th.CPAGroupID, []string{publicField.ID, sourceOnlyField.ID, sharedOnlyField.ID})
|
|
require.NoError(t, err)
|
|
require.Len(t, fields, 3)
|
|
|
|
// Find each field and verify
|
|
for _, field := range fields {
|
|
if field.ID == sourceOnlyField.ID {
|
|
// Source plugin sees all options
|
|
assert.Len(t, field.Attrs[model.PropertyFieldAttributeOptions].([]any), 1)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("user sees all fields with filtered options", func(t *testing.T) {
|
|
fields, err := th.service.GetPropertyFields(rctxUser, th.CPAGroupID, []string{publicField.ID, sourceOnlyField.ID, sharedOnlyField.ID})
|
|
require.NoError(t, err)
|
|
require.Len(t, fields, 3)
|
|
|
|
// Find each field and verify
|
|
for _, field := range fields {
|
|
if field.ID == sourceOnlyField.ID {
|
|
// User sees empty options for source_only
|
|
assert.Empty(t, field.Attrs[model.PropertyFieldAttributeOptions].([]any))
|
|
} else if field.ID == sharedOnlyField.ID {
|
|
// User sees filtered options (only opt1)
|
|
assert.Len(t, field.Attrs[model.PropertyFieldAttributeOptions].([]any), 1)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestSearchPropertyFieldsReadAccess(t *testing.T) {
|
|
th := Setup(t).RegisterCPAPropertyGroup(t)
|
|
th.service.setPluginCheckerForTests(func(pluginID string) bool {
|
|
return pluginID == "plugin-1" || pluginID == "test-plugin"
|
|
})
|
|
|
|
pluginID := "plugin-1"
|
|
userID := model.NewId()
|
|
|
|
rctxAnon := RequestContextWithCallerID(th.Context, "")
|
|
rctxPlugin := RequestContextWithCallerID(th.Context, pluginID)
|
|
rctxUser := RequestContextWithCallerID(th.Context, userID)
|
|
rctxTestPlugin := RequestContextWithCallerID(th.Context, "test-plugin")
|
|
|
|
// Create fields with different access modes
|
|
publicField := &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
Name: "public-search-field",
|
|
Type: model.PropertyFieldTypeText,
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
Attrs: model.StringInterface{
|
|
model.PropertyAttrsAccessMode: model.PropertyAccessModePublic,
|
|
},
|
|
}
|
|
_, err := th.service.CreatePropertyField(rctxAnon, publicField)
|
|
require.NoError(t, err)
|
|
|
|
sourceOnlyField := &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
Name: "source-search-field",
|
|
Type: model.PropertyFieldTypeSelect,
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
Attrs: model.StringInterface{
|
|
model.PropertyAttrsAccessMode: model.PropertyAccessModeSourceOnly,
|
|
model.PropertyAttrsProtected: true,
|
|
model.PropertyFieldAttributeOptions: []any{
|
|
map[string]any{"id": "opt1", "value": "Secret"},
|
|
},
|
|
},
|
|
}
|
|
_, err = th.service.CreatePropertyField(rctxPlugin, sourceOnlyField)
|
|
require.NoError(t, err)
|
|
|
|
sharedOnlyField := &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
Name: "shared-search-field",
|
|
Type: model.PropertyFieldTypeMultiselect,
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
Attrs: model.StringInterface{
|
|
model.PropertyAttrsAccessMode: model.PropertyAccessModeSharedOnly,
|
|
model.PropertyAttrsProtected: true,
|
|
model.PropertyFieldAttributeOptions: []any{
|
|
map[string]any{"id": "opt1", "value": "Option 1"},
|
|
map[string]any{"id": "opt2", "value": "Option 2"},
|
|
},
|
|
},
|
|
}
|
|
sharedOnlyField, err = th.service.CreatePropertyField(rctxTestPlugin, sharedOnlyField)
|
|
require.NoError(t, err)
|
|
|
|
// Create value for userID (opt1)
|
|
value, jsonErr := json.Marshal([]string{"opt1"})
|
|
require.NoError(t, jsonErr)
|
|
_, err = th.service.CreatePropertyValue(rctxTestPlugin, &model.PropertyValue{
|
|
GroupID: th.CPAGroupID,
|
|
FieldID: sharedOnlyField.ID,
|
|
TargetType: "user",
|
|
TargetID: userID,
|
|
Value: value,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
t.Run("search returns all fields with appropriate filtering", func(t *testing.T) {
|
|
// User search
|
|
results, err := th.service.SearchPropertyFields(rctxUser, th.CPAGroupID, model.PropertyFieldSearchOpts{
|
|
PerPage: 100,
|
|
})
|
|
require.NoError(t, err)
|
|
assert.GreaterOrEqual(t, len(results), 3)
|
|
|
|
// Verify filtering
|
|
for _, field := range results {
|
|
if field.Name == "source-search-field" {
|
|
// User sees empty options for source_only
|
|
assert.Empty(t, field.Attrs[model.PropertyFieldAttributeOptions].([]any))
|
|
} else if field.Name == "shared-search-field" {
|
|
// User sees filtered options (only opt1)
|
|
options := field.Attrs[model.PropertyFieldAttributeOptions].([]any)
|
|
assert.Len(t, options, 1)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("source plugin search sees unfiltered options", func(t *testing.T) {
|
|
results, err := th.service.SearchPropertyFields(rctxPlugin, th.CPAGroupID, model.PropertyFieldSearchOpts{
|
|
PerPage: 100,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Verify source plugin sees all options
|
|
for _, field := range results {
|
|
if field.Name == "source-search-field" {
|
|
assert.Len(t, field.Attrs[model.PropertyFieldAttributeOptions].([]any), 1)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestGetPropertyFieldByNameReadAccess(t *testing.T) {
|
|
th := Setup(t).RegisterCPAPropertyGroup(t)
|
|
th.service.setPluginCheckerForTests(func(pluginID string) bool {
|
|
return pluginID == "plugin-1"
|
|
})
|
|
|
|
pluginID := "plugin-1"
|
|
userID := model.NewId()
|
|
|
|
rctxPlugin := RequestContextWithCallerID(th.Context, pluginID)
|
|
rctxUser := RequestContextWithCallerID(th.Context, userID)
|
|
|
|
t.Run("source_only field by name - filters options for non-source", func(t *testing.T) {
|
|
field := &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
Name: "byname-source-only",
|
|
Type: model.PropertyFieldTypeSelect,
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
Attrs: model.StringInterface{
|
|
model.PropertyAttrsAccessMode: model.PropertyAccessModeSourceOnly,
|
|
model.PropertyAttrsProtected: true,
|
|
model.PropertyFieldAttributeOptions: []any{
|
|
map[string]any{"id": "opt1", "value": "Secret"},
|
|
},
|
|
},
|
|
}
|
|
created, err := th.service.CreatePropertyField(rctxPlugin, field)
|
|
require.NoError(t, err)
|
|
|
|
// Source plugin can see options
|
|
retrieved, err := th.service.GetPropertyFieldByName(rctxPlugin, th.CPAGroupID, "", created.Name)
|
|
require.NoError(t, err)
|
|
assert.Len(t, retrieved.Attrs[model.PropertyFieldAttributeOptions].([]any), 1)
|
|
|
|
// User sees empty options
|
|
retrieved, err = th.service.GetPropertyFieldByName(rctxUser, th.CPAGroupID, "", created.Name)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, retrieved.Attrs[model.PropertyFieldAttributeOptions].([]any))
|
|
})
|
|
}
|
|
|
|
// TestCreatePropertyField_AccessControl tests access control for field creation based on caller type
|
|
func TestCreatePropertyField_AccessControl(t *testing.T) {
|
|
th := Setup(t).RegisterCPAPropertyGroup(t)
|
|
th.service.setPluginCheckerForTests(func(pluginID string) bool {
|
|
return pluginID == "plugin-1" || pluginID == "security-plugin"
|
|
})
|
|
|
|
rctxAnon := RequestContextWithCallerID(th.Context, "")
|
|
rctxUser1 := RequestContextWithCallerID(th.Context, "user-1")
|
|
rctxPlugin1 := RequestContextWithCallerID(th.Context, "plugin-1")
|
|
rctxSecurityPlugin := RequestContextWithCallerID(th.Context, "security-plugin")
|
|
|
|
// Non-plugin caller tests
|
|
|
|
t.Run("non-plugin caller can create field without source_plugin_id", func(t *testing.T) {
|
|
field := &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
}
|
|
|
|
created, err := th.service.CreatePropertyField(rctxUser1, field)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, created)
|
|
// source_plugin_id is not set for non-plugin callers
|
|
assert.Empty(t, created.Attrs[model.PropertyAttrsSourcePluginID])
|
|
})
|
|
|
|
t.Run("non-plugin caller cannot set source_plugin_id", func(t *testing.T) {
|
|
field := &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
Attrs: model.StringInterface{
|
|
model.PropertyAttrsSourcePluginID: "plugin-1",
|
|
},
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
}
|
|
|
|
created, err := th.service.CreatePropertyField(RequestContextWithCallerID(th.Context, "user-id-123"), field)
|
|
require.Error(t, err)
|
|
assert.Nil(t, created)
|
|
assert.Contains(t, err.Error(), "source_plugin_id can only be set by a plugin")
|
|
})
|
|
|
|
t.Run("non-plugin caller cannot set protected", func(t *testing.T) {
|
|
field := &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
Attrs: model.StringInterface{
|
|
model.PropertyAttrsProtected: true,
|
|
},
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
}
|
|
|
|
created, err := th.service.CreatePropertyField(rctxUser1, field)
|
|
require.Error(t, err)
|
|
assert.Nil(t, created)
|
|
assert.Contains(t, err.Error(), "protected can only be set by a plugin")
|
|
})
|
|
|
|
t.Run("anonymous caller cannot set source_plugin_id", func(t *testing.T) {
|
|
field := &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
Attrs: model.StringInterface{
|
|
model.PropertyAttrsSourcePluginID: "plugin-1",
|
|
},
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
}
|
|
|
|
created, err := th.service.CreatePropertyField(rctxAnon, field)
|
|
require.Error(t, err)
|
|
assert.Nil(t, created)
|
|
assert.Contains(t, err.Error(), "source_plugin_id can only be set by a plugin")
|
|
})
|
|
|
|
t.Run("anonymous caller cannot set protected", func(t *testing.T) {
|
|
field := &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
Attrs: model.StringInterface{
|
|
model.PropertyAttrsProtected: true,
|
|
},
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
}
|
|
|
|
created, err := th.service.CreatePropertyField(rctxAnon, field)
|
|
require.Error(t, err)
|
|
assert.Nil(t, created)
|
|
assert.Contains(t, err.Error(), "protected can only be set by a plugin")
|
|
})
|
|
|
|
t.Run("anonymous caller can create field with empty source_plugin_id", func(t *testing.T) {
|
|
field := &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
Attrs: model.StringInterface{
|
|
model.PropertyAttrsSourcePluginID: "",
|
|
},
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
}
|
|
|
|
created, err := th.service.CreatePropertyField(rctxAnon, field)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, created)
|
|
})
|
|
|
|
// Plugin caller tests
|
|
|
|
t.Run("plugin caller auto-sets source_plugin_id", func(t *testing.T) {
|
|
field := &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
}
|
|
|
|
created, err := th.service.CreatePropertyField(rctxPlugin1, field)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, created)
|
|
assert.Equal(t, "plugin-1", created.Attrs[model.PropertyAttrsSourcePluginID])
|
|
})
|
|
|
|
t.Run("plugin caller overwrites any pre-set source_plugin_id", func(t *testing.T) {
|
|
field := &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
Attrs: model.StringInterface{
|
|
model.PropertyAttrsSourcePluginID: "malicious-plugin",
|
|
},
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
}
|
|
|
|
created, err := th.service.CreatePropertyField(rctxPlugin1, field)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, created)
|
|
assert.Equal(t, "plugin-1", created.Attrs[model.PropertyAttrsSourcePluginID])
|
|
})
|
|
|
|
t.Run("plugin caller can create protected field with source_only access mode", func(t *testing.T) {
|
|
field := &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeSelect,
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
Attrs: model.StringInterface{
|
|
model.PropertyAttrsProtected: true,
|
|
model.PropertyAttrsAccessMode: model.PropertyAccessModeSourceOnly,
|
|
model.PropertyFieldAttributeOptions: []any{
|
|
map[string]any{"id": "opt1", "value": "Secret Option"},
|
|
},
|
|
},
|
|
}
|
|
|
|
created, err := th.service.CreatePropertyField(rctxSecurityPlugin, field)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, created)
|
|
assert.Equal(t, "security-plugin", created.Attrs[model.PropertyAttrsSourcePluginID])
|
|
assert.True(t, created.Attrs[model.PropertyAttrsProtected].(bool))
|
|
assert.Equal(t, model.PropertyAccessModeSourceOnly, created.Attrs[model.PropertyAttrsAccessMode])
|
|
})
|
|
|
|
t.Run("non-CPA group routes directly to PropertyService without setting source_plugin_id", func(t *testing.T) {
|
|
nonCpaGroup, err := th.service.RegisterPropertyGroup(&model.PropertyGroup{Name: "other_group_create", Version: model.PropertyGroupVersionV2})
|
|
require.NoError(t, err)
|
|
|
|
field := &model.PropertyField{
|
|
GroupID: nonCpaGroup.ID,
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
}
|
|
|
|
rctx := RequestContextWithCallerID(th.Context, "plugin-2")
|
|
created, err := th.service.CreatePropertyField(rctx, field)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, created)
|
|
assert.Nil(t, created.Attrs[model.PropertyAttrsSourcePluginID])
|
|
})
|
|
}
|
|
|
|
// TestUpdatePropertyField_WriteAccessControl tests write access control for field updates
|
|
func TestUpdatePropertyField_WriteAccessControl(t *testing.T) {
|
|
th := Setup(t).RegisterCPAPropertyGroup(t)
|
|
|
|
th.service.setPluginCheckerForTests(func(pluginID string) bool {
|
|
return pluginID == "plugin-1" || pluginID == "plugin-2"
|
|
})
|
|
|
|
rctxPlugin1 := RequestContextWithCallerID(th.Context, "plugin-1")
|
|
rctxPlugin2 := RequestContextWithCallerID(th.Context, "plugin-2")
|
|
rctxAnon := RequestContextWithCallerID(th.Context, "")
|
|
|
|
t.Run("allows update of unprotected field", func(t *testing.T) {
|
|
field := &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
Name: "Original Name",
|
|
Type: model.PropertyFieldTypeText,
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
}
|
|
|
|
created, err := th.service.CreatePropertyField(rctxPlugin1, field)
|
|
require.NoError(t, err)
|
|
|
|
created.Name = "Updated Name"
|
|
updated, _, err := th.service.UpdatePropertyField(rctxPlugin2, th.CPAGroupID, created)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "Updated Name", updated.Name)
|
|
})
|
|
|
|
t.Run("allows source plugin to update protected field", func(t *testing.T) {
|
|
field := &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
Name: "Protected Field",
|
|
Type: model.PropertyFieldTypeText,
|
|
Attrs: model.StringInterface{
|
|
model.PropertyAttrsProtected: true,
|
|
},
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
}
|
|
|
|
created, err := th.service.CreatePropertyField(rctxPlugin1, field)
|
|
require.NoError(t, err)
|
|
|
|
created.Name = "Updated Protected Field"
|
|
updated, _, err := th.service.UpdatePropertyField(rctxPlugin1, th.CPAGroupID, created)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "Updated Protected Field", updated.Name)
|
|
})
|
|
|
|
t.Run("denies non-source plugin updating protected field", func(t *testing.T) {
|
|
field := &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
Name: "Protected Field",
|
|
Type: model.PropertyFieldTypeText,
|
|
Attrs: model.StringInterface{
|
|
model.PropertyAttrsProtected: true,
|
|
},
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
}
|
|
|
|
created, err := th.service.CreatePropertyField(rctxPlugin1, field)
|
|
require.NoError(t, err)
|
|
|
|
created.Name = "Attempted Update"
|
|
updated, _, err := th.service.UpdatePropertyField(rctxPlugin2, th.CPAGroupID, created)
|
|
require.Error(t, err)
|
|
assert.Nil(t, updated)
|
|
assert.Contains(t, err.Error(), "protected")
|
|
assert.Contains(t, err.Error(), "plugin-1")
|
|
})
|
|
|
|
t.Run("denies empty callerID updating protected field", func(t *testing.T) {
|
|
field := &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
Name: "Protected Field Empty Caller",
|
|
Type: model.PropertyFieldTypeText,
|
|
Attrs: model.StringInterface{
|
|
model.PropertyAttrsProtected: true,
|
|
},
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
}
|
|
|
|
created, err := th.service.CreatePropertyField(rctxPlugin1, field)
|
|
require.NoError(t, err)
|
|
|
|
created.Name = "Attempted Update"
|
|
updated, _, err := th.service.UpdatePropertyField(rctxAnon, th.CPAGroupID, created)
|
|
require.Error(t, err)
|
|
assert.Nil(t, updated)
|
|
assert.Contains(t, err.Error(), "protected")
|
|
})
|
|
|
|
t.Run("prevents changing source_plugin_id", func(t *testing.T) {
|
|
field := &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
Name: "Field",
|
|
Type: model.PropertyFieldTypeText,
|
|
Attrs: model.StringInterface{},
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
}
|
|
|
|
created, err := th.service.CreatePropertyField(rctxPlugin1, field)
|
|
require.NoError(t, err)
|
|
|
|
// Try to change source_plugin_id
|
|
created.Attrs[model.PropertyAttrsSourcePluginID] = "plugin-2"
|
|
updated, _, err := th.service.UpdatePropertyField(rctxPlugin1, th.CPAGroupID, created)
|
|
require.Error(t, err)
|
|
assert.Nil(t, updated)
|
|
assert.Contains(t, err.Error(), "immutable")
|
|
})
|
|
|
|
t.Run("prevents setting protected=true without source_plugin_id", func(t *testing.T) {
|
|
field := &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
Name: "Field Without Source Plugin",
|
|
Type: model.PropertyFieldTypeText,
|
|
Attrs: model.StringInterface{},
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
}
|
|
|
|
created, err := th.service.CreatePropertyField(rctxAnon, field)
|
|
require.NoError(t, err)
|
|
|
|
// Try to set protected=true without having a source_plugin_id
|
|
created.Attrs[model.PropertyAttrsProtected] = true
|
|
updated, _, err := th.service.UpdatePropertyField(rctxPlugin1, th.CPAGroupID, created)
|
|
require.Error(t, err)
|
|
assert.Nil(t, updated)
|
|
assert.Contains(t, err.Error(), "cannot set protected=true")
|
|
assert.Contains(t, err.Error(), "source_plugin_id")
|
|
})
|
|
|
|
t.Run("prevents non-source plugin from setting protected=true", func(t *testing.T) {
|
|
field := &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
Name: "Field With Source Plugin",
|
|
Type: model.PropertyFieldTypeText,
|
|
Attrs: model.StringInterface{},
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
}
|
|
|
|
// Create field via plugin-1 (sets source_plugin_id automatically)
|
|
created, err := th.service.CreatePropertyField(rctxPlugin1, field)
|
|
require.NoError(t, err)
|
|
assert.False(t, model.IsPropertyFieldProtected(created))
|
|
|
|
// Try to set protected=true by a different plugin (plugin-2)
|
|
created.Attrs[model.PropertyAttrsProtected] = true
|
|
updated, _, err := th.service.UpdatePropertyField(rctxPlugin2, th.CPAGroupID, created)
|
|
require.Error(t, err)
|
|
assert.Nil(t, updated)
|
|
assert.Contains(t, err.Error(), "cannot set protected=true")
|
|
assert.Contains(t, err.Error(), "plugin-1")
|
|
|
|
// Verify the source plugin (plugin-1) CAN set protected=true
|
|
updated, _, err = th.service.UpdatePropertyField(rctxPlugin1, th.CPAGroupID, created)
|
|
require.NoError(t, err)
|
|
assert.True(t, model.IsPropertyFieldProtected(updated))
|
|
})
|
|
|
|
t.Run("non-CPA group routes directly to PropertyService without access control", func(t *testing.T) {
|
|
nonCpaGroup, err := th.service.RegisterPropertyGroup(&model.PropertyGroup{Name: "other_group_update", Version: model.PropertyGroupVersionV2})
|
|
require.NoError(t, err)
|
|
|
|
field := &model.PropertyField{
|
|
GroupID: nonCpaGroup.ID,
|
|
Name: "Non-CPA Protected",
|
|
Type: model.PropertyFieldTypeText,
|
|
Attrs: model.StringInterface{
|
|
model.PropertyAttrsProtected: true,
|
|
model.PropertyAttrsSourcePluginID: "plugin-1",
|
|
},
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
}
|
|
|
|
created, err := th.service.CreatePropertyField(rctxPlugin1, field)
|
|
require.NoError(t, err)
|
|
|
|
// Update with different plugin - should be allowed (no access control)
|
|
created.Name = "Updated by Plugin2"
|
|
updated, _, err := th.service.UpdatePropertyField(rctxPlugin2, nonCpaGroup.ID, created)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, updated)
|
|
assert.Equal(t, "Updated by Plugin2", updated.Name)
|
|
})
|
|
}
|
|
|
|
// TestUpdatePropertyFields_BulkWriteAccessControl tests bulk field updates with atomic access checking
|
|
func TestUpdatePropertyFields_BulkWriteAccessControl(t *testing.T) {
|
|
th := Setup(t).RegisterCPAPropertyGroup(t)
|
|
|
|
th.service.setPluginCheckerForTests(func(pluginID string) bool {
|
|
return pluginID == "plugin-1" || pluginID == "plugin-2"
|
|
})
|
|
|
|
rctxPlugin1 := RequestContextWithCallerID(th.Context, "plugin-1")
|
|
rctxPlugin2 := RequestContextWithCallerID(th.Context, "plugin-2")
|
|
|
|
t.Run("allows bulk update of unprotected fields", func(t *testing.T) {
|
|
field1 := &model.PropertyField{GroupID: th.CPAGroupID, Name: "Field1", Type: model.PropertyFieldTypeText, ObjectType: model.PropertyFieldObjectTypeUser, TargetType: string(model.PropertyFieldTargetLevelSystem)}
|
|
field2 := &model.PropertyField{GroupID: th.CPAGroupID, Name: "Field2", Type: model.PropertyFieldTypeText, ObjectType: model.PropertyFieldObjectTypeUser, TargetType: string(model.PropertyFieldTargetLevelSystem)}
|
|
|
|
created1, err := th.service.CreatePropertyField(rctxPlugin1, field1)
|
|
require.NoError(t, err)
|
|
created2, err := th.service.CreatePropertyField(rctxPlugin1, field2)
|
|
require.NoError(t, err)
|
|
|
|
created1.Name = "Updated Field1"
|
|
created2.Name = "Updated Field2"
|
|
|
|
updated, _, _, err := th.service.UpdatePropertyFields(rctxPlugin2, th.CPAGroupID, []*model.PropertyField{created1, created2})
|
|
require.NoError(t, err)
|
|
assert.Len(t, updated, 2)
|
|
})
|
|
|
|
t.Run("fails atomically when one protected field in batch", func(t *testing.T) {
|
|
// Create unprotected field
|
|
field1 := &model.PropertyField{GroupID: th.CPAGroupID, Name: "Unprotected", Type: model.PropertyFieldTypeText, ObjectType: model.PropertyFieldObjectTypeUser, TargetType: string(model.PropertyFieldTargetLevelSystem)}
|
|
created1, err := th.service.CreatePropertyField(rctxPlugin1, field1)
|
|
require.NoError(t, err)
|
|
|
|
// Create protected field
|
|
field2 := &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
Name: "Protected",
|
|
Type: model.PropertyFieldTypeText,
|
|
Attrs: model.StringInterface{
|
|
model.PropertyAttrsProtected: true,
|
|
},
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
}
|
|
created2, err := th.service.CreatePropertyField(rctxPlugin1, field2)
|
|
require.NoError(t, err)
|
|
|
|
// Try to update both with plugin2 (should fail atomically)
|
|
created1.Name = "Updated Unprotected"
|
|
created2.Name = "Updated Protected"
|
|
|
|
updated, _, _, err := th.service.UpdatePropertyFields(rctxPlugin2, th.CPAGroupID, []*model.PropertyField{created1, created2})
|
|
require.Error(t, err)
|
|
assert.Nil(t, updated)
|
|
assert.Contains(t, err.Error(), "protected")
|
|
|
|
// Verify neither was updated
|
|
check1, err := th.service.GetPropertyField(rctxPlugin1, th.CPAGroupID, created1.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "Unprotected", check1.Name)
|
|
|
|
check2, err := th.service.GetPropertyField(rctxPlugin1, th.CPAGroupID, created2.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "Protected", check2.Name)
|
|
})
|
|
|
|
t.Run("fails atomically when trying to set protected=true without source_plugin_id in batch", func(t *testing.T) {
|
|
rctxAnon := RequestContextWithCallerID(th.Context, "")
|
|
|
|
// Create two unprotected fields without source_plugin_id
|
|
field1 := &model.PropertyField{GroupID: th.CPAGroupID, Name: "Field1", Type: model.PropertyFieldTypeText, Attrs: model.StringInterface{}, ObjectType: model.PropertyFieldObjectTypeUser, TargetType: string(model.PropertyFieldTargetLevelSystem)}
|
|
field2 := &model.PropertyField{GroupID: th.CPAGroupID, Name: "Field2", Type: model.PropertyFieldTypeText, Attrs: model.StringInterface{}, ObjectType: model.PropertyFieldObjectTypeUser, TargetType: string(model.PropertyFieldTargetLevelSystem)}
|
|
|
|
created1, err := th.service.CreatePropertyField(rctxAnon, field1)
|
|
require.NoError(t, err)
|
|
created2, err := th.service.CreatePropertyField(rctxAnon, field2)
|
|
require.NoError(t, err)
|
|
|
|
// Try to set protected=true on field2 without source_plugin_id
|
|
created1.Name = "Updated Field1"
|
|
created2.Attrs[model.PropertyAttrsProtected] = true
|
|
|
|
updated, _, _, err := th.service.UpdatePropertyFields(rctxPlugin1, th.CPAGroupID, []*model.PropertyField{created1, created2})
|
|
require.Error(t, err)
|
|
assert.Nil(t, updated)
|
|
assert.Contains(t, err.Error(), "cannot set protected=true")
|
|
assert.Contains(t, err.Error(), "source_plugin_id")
|
|
|
|
// Verify neither was updated (atomic failure)
|
|
check1, err := th.service.GetPropertyField(rctxPlugin1, th.CPAGroupID, created1.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "Field1", check1.Name)
|
|
|
|
check2, err := th.service.GetPropertyField(rctxPlugin1, th.CPAGroupID, created2.ID)
|
|
require.NoError(t, err)
|
|
assert.False(t, model.IsPropertyFieldProtected(check2))
|
|
})
|
|
}
|
|
|
|
// TestDeletePropertyField_WriteAccessControl tests write access control for field deletion
|
|
func TestDeletePropertyField_WriteAccessControl(t *testing.T) {
|
|
th := Setup(t).RegisterCPAPropertyGroup(t)
|
|
th.service.setPluginCheckerForTests(func(pluginID string) bool { return pluginID == "plugin-1" })
|
|
|
|
rctxPlugin1 := RequestContextWithCallerID(th.Context, "plugin-1")
|
|
rctxPlugin2 := RequestContextWithCallerID(th.Context, "plugin-2")
|
|
|
|
t.Run("allows deletion of unprotected field", func(t *testing.T) {
|
|
field := &model.PropertyField{GroupID: th.CPAGroupID, Name: "Unprotected", Type: model.PropertyFieldTypeText, ObjectType: model.PropertyFieldObjectTypeUser, TargetType: string(model.PropertyFieldTargetLevelSystem)}
|
|
created, err := th.service.CreatePropertyField(rctxPlugin1, field)
|
|
require.NoError(t, err)
|
|
|
|
err = th.service.DeletePropertyField(rctxPlugin2, th.CPAGroupID, created.ID)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("allows source plugin to delete protected field", func(t *testing.T) {
|
|
field := &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
Name: "Protected",
|
|
Type: model.PropertyFieldTypeText,
|
|
Attrs: model.StringInterface{
|
|
model.PropertyAttrsProtected: true,
|
|
},
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
}
|
|
created, err := th.service.CreatePropertyField(rctxPlugin1, field)
|
|
require.NoError(t, err)
|
|
|
|
err = th.service.DeletePropertyField(rctxPlugin1, th.CPAGroupID, created.ID)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("denies non-source plugin deleting protected field", func(t *testing.T) {
|
|
th.service.setPluginCheckerForTests(func(pluginID string) bool {
|
|
return pluginID == "plugin-1"
|
|
})
|
|
|
|
field := &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
Name: "Protected",
|
|
Type: model.PropertyFieldTypeText,
|
|
Attrs: model.StringInterface{
|
|
model.PropertyAttrsProtected: true,
|
|
},
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
}
|
|
created, err := th.service.CreatePropertyField(rctxPlugin1, field)
|
|
require.NoError(t, err)
|
|
|
|
err = th.service.DeletePropertyField(rctxPlugin2, th.CPAGroupID, created.ID)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "protected")
|
|
})
|
|
|
|
t.Run("non-CPA group routes directly to PropertyService without access control", func(t *testing.T) {
|
|
nonCpaGroup, err := th.service.RegisterPropertyGroup(&model.PropertyGroup{Name: "other_group_delete", Version: model.PropertyGroupVersionV2})
|
|
require.NoError(t, err)
|
|
|
|
field := &model.PropertyField{
|
|
GroupID: nonCpaGroup.ID,
|
|
Name: "Non-CPA Delete Protected",
|
|
Type: model.PropertyFieldTypeText,
|
|
Attrs: model.StringInterface{
|
|
model.PropertyAttrsProtected: true,
|
|
model.PropertyAttrsSourcePluginID: "plugin-1",
|
|
},
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
}
|
|
|
|
created, err := th.service.CreatePropertyField(rctxPlugin1, field)
|
|
require.NoError(t, err)
|
|
|
|
// Delete with different plugin - should be allowed (no access control)
|
|
err = th.service.DeletePropertyField(rctxPlugin2, nonCpaGroup.ID, created.ID)
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
|
|
func TestDeletePropertyField_OrphanedFieldDeletion(t *testing.T) {
|
|
th := Setup(t).RegisterCPAPropertyGroup(t)
|
|
th.service.setPluginCheckerForTests(func(pluginID string) bool { return pluginID == "plugin-1" })
|
|
|
|
t.Run("allows deletion of orphaned protected field when plugin is uninstalled", func(t *testing.T) {
|
|
// Create the field while the plugin is still installed
|
|
th.service.setPluginCheckerForTests(func(pluginID string) bool {
|
|
return pluginID == "removed-plugin"
|
|
})
|
|
|
|
field := &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
Name: "Orphaned Protected Field",
|
|
Type: model.PropertyFieldTypeText,
|
|
Attrs: model.StringInterface{
|
|
model.PropertyAttrsProtected: true,
|
|
},
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
}
|
|
created, err := th.service.CreatePropertyField(RequestContextWithCallerID(th.Context, "removed-plugin"), field)
|
|
require.NoError(t, err)
|
|
|
|
// Simulate plugin uninstall
|
|
th.service.setPluginCheckerForTests(func(pluginID string) bool {
|
|
return false
|
|
})
|
|
|
|
err = th.service.DeletePropertyField(RequestContextWithCallerID(th.Context, "admin-user"), th.CPAGroupID, created.ID)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("blocks deletion of protected field when plugin is still installed", func(t *testing.T) {
|
|
th.service.setPluginCheckerForTests(func(pluginID string) bool {
|
|
return pluginID == "installed-plugin"
|
|
})
|
|
|
|
field := &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
Name: "Active Protected Field",
|
|
Type: model.PropertyFieldTypeText,
|
|
Attrs: model.StringInterface{
|
|
model.PropertyAttrsProtected: true,
|
|
},
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
}
|
|
created, err := th.service.CreatePropertyField(RequestContextWithCallerID(th.Context, "installed-plugin"), field)
|
|
require.NoError(t, err)
|
|
|
|
err = th.service.DeletePropertyField(RequestContextWithCallerID(th.Context, "admin-user"), th.CPAGroupID, created.ID)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "protected")
|
|
assert.Contains(t, err.Error(), "installed-plugin")
|
|
|
|
err = th.service.DeletePropertyField(RequestContextWithCallerID(th.Context, "installed-plugin"), th.CPAGroupID, created.ID)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("blocks update of orphaned protected field even when plugin is uninstalled", func(t *testing.T) {
|
|
// Create the field while the plugin is still installed
|
|
th.service.setPluginCheckerForTests(func(pluginID string) bool {
|
|
return pluginID == "removed-plugin"
|
|
})
|
|
|
|
field := &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
Name: "Orphaned Field For Update",
|
|
Type: model.PropertyFieldTypeText,
|
|
Attrs: model.StringInterface{
|
|
model.PropertyAttrsProtected: true,
|
|
},
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
}
|
|
created, err := th.service.CreatePropertyField(RequestContextWithCallerID(th.Context, "removed-plugin"), field)
|
|
require.NoError(t, err)
|
|
|
|
// Simulate plugin uninstall
|
|
th.service.setPluginCheckerForTests(func(pluginID string) bool {
|
|
return false
|
|
})
|
|
|
|
created.Name = "Updated Orphaned Field"
|
|
updated, _, err := th.service.UpdatePropertyField(RequestContextWithCallerID(th.Context, "admin-user"), th.CPAGroupID, created)
|
|
require.Error(t, err)
|
|
assert.Nil(t, updated)
|
|
assert.Contains(t, err.Error(), "protected")
|
|
})
|
|
}
|
|
|
|
func TestLinkedPropertyField_SecurityInheritance(t *testing.T) {
|
|
t.Skip("Skip until CPA is fully migrated to v2: this test creates linked fields with ObjectType and permissions on a v1 group, which is rejected by the version mismatch check")
|
|
|
|
th := Setup(t).RegisterCPAPropertyGroup(t)
|
|
th.service.setPluginCheckerForTests(func(pluginID string) bool {
|
|
return pluginID == "plugin-1" || pluginID == "plugin-2"
|
|
})
|
|
|
|
rctxPlugin1 := RequestContextWithCallerID(th.Context, "plugin-1")
|
|
rctxPlugin2 := RequestContextWithCallerID(th.Context, "plugin-2")
|
|
rctxUser := RequestContextWithCallerID(th.Context, "user-1")
|
|
|
|
sysadminLevel := model.PermissionLevelSysadmin
|
|
|
|
// Helper: create a protected template in the CPA group owned by plugin-1
|
|
createProtectedTemplate := func(t *testing.T, name string, accessMode string) *model.PropertyField {
|
|
t.Helper()
|
|
attrs := model.StringInterface{
|
|
model.PropertyAttrsProtected: true,
|
|
model.PropertyAttrsAccessMode: accessMode,
|
|
model.PropertyFieldAttributeOptions: []any{
|
|
map[string]any{"id": model.NewId(), "name": "Option A"},
|
|
map[string]any{"id": model.NewId(), "name": "Option B"},
|
|
},
|
|
}
|
|
field := &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
ObjectType: model.PropertyFieldObjectTypeTemplate,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
Type: model.PropertyFieldTypeSelect,
|
|
Name: name,
|
|
Attrs: attrs,
|
|
PermissionField: &sysadminLevel,
|
|
PermissionValues: &sysadminLevel,
|
|
PermissionOptions: &sysadminLevel,
|
|
}
|
|
created, err := th.service.CreatePropertyField(rctxPlugin1, field)
|
|
require.NoError(t, err)
|
|
return created
|
|
}
|
|
|
|
// Helper: create an unprotected template in the CPA group
|
|
createUnprotectedTemplate := func(t *testing.T, name string) *model.PropertyField {
|
|
t.Helper()
|
|
field := &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
ObjectType: model.PropertyFieldObjectTypeTemplate,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
Type: model.PropertyFieldTypeSelect,
|
|
Name: name,
|
|
Attrs: model.StringInterface{
|
|
model.PropertyFieldAttributeOptions: []any{
|
|
map[string]any{"id": model.NewId(), "name": "Option A"},
|
|
},
|
|
},
|
|
}
|
|
created, err := th.service.CreatePropertyField(rctxPlugin1, field)
|
|
require.NoError(t, err)
|
|
return created
|
|
}
|
|
|
|
t.Run("source plugin can create linked field from protected template", func(t *testing.T) {
|
|
source := createProtectedTemplate(t, "ProtectedSource-"+model.NewId(), model.PropertyAccessModeSourceOnly)
|
|
|
|
linked, err := th.service.CreatePropertyField(rctxPlugin1, &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
Name: "Linked-" + model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
LinkedFieldID: &source.ID,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Verify security attrs inherited
|
|
assert.True(t, model.IsPropertyFieldProtected(linked))
|
|
assert.Equal(t, "plugin-1", linked.Attrs[model.PropertyAttrsSourcePluginID])
|
|
assert.Equal(t, model.PropertyAccessModeSourceOnly, linked.Attrs[model.PropertyAttrsAccessMode])
|
|
})
|
|
|
|
t.Run("different plugin cannot create linked field from protected template", func(t *testing.T) {
|
|
source := createProtectedTemplate(t, "ProtectedSource-"+model.NewId(), model.PropertyAccessModeSourceOnly)
|
|
|
|
_, err := th.service.CreatePropertyField(rctxPlugin2, &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
Name: "Linked-" + model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
LinkedFieldID: &source.ID,
|
|
})
|
|
require.Error(t, err)
|
|
var appErr *model.AppError
|
|
require.ErrorAs(t, err, &appErr)
|
|
assert.Equal(t, http.StatusForbidden, appErr.StatusCode)
|
|
assert.Contains(t, appErr.DetailedError, "only the source plugin")
|
|
})
|
|
|
|
t.Run("non-plugin caller cannot create linked field from protected template", func(t *testing.T) {
|
|
source := createProtectedTemplate(t, "ProtectedSource-"+model.NewId(), model.PropertyAccessModeSourceOnly)
|
|
|
|
_, err := th.service.CreatePropertyField(rctxUser, &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
Name: "Linked-" + model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
LinkedFieldID: &source.ID,
|
|
})
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "only the source plugin")
|
|
})
|
|
|
|
t.Run("linked field inherits Protected and attrs from source", func(t *testing.T) {
|
|
source := createProtectedTemplate(t, "InheritSource-"+model.NewId(), model.PropertyAccessModeSourceOnly)
|
|
|
|
linked, err := th.service.CreatePropertyField(rctxPlugin1, &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
Name: "Linked-" + model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
LinkedFieldID: &source.ID,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, source.Protected, linked.Protected)
|
|
assert.Equal(t, source.Attrs[model.PropertyAttrsProtected], linked.Attrs[model.PropertyAttrsProtected])
|
|
assert.Equal(t, source.Attrs[model.PropertyAttrsSourcePluginID], linked.Attrs[model.PropertyAttrsSourcePluginID])
|
|
assert.Equal(t, source.Attrs[model.PropertyAttrsAccessMode], linked.Attrs[model.PropertyAttrsAccessMode])
|
|
})
|
|
|
|
t.Run("linked field inherits permission levels from source", func(t *testing.T) {
|
|
source := createProtectedTemplate(t, "PermSource-"+model.NewId(), model.PropertyAccessModeSourceOnly)
|
|
|
|
linked, err := th.service.CreatePropertyField(rctxPlugin1, &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
Name: "Linked-" + model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
LinkedFieldID: &source.ID,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
require.NotNil(t, linked.PermissionField)
|
|
require.NotNil(t, linked.PermissionValues)
|
|
require.NotNil(t, linked.PermissionOptions)
|
|
assert.Equal(t, *source.PermissionField, *linked.PermissionField)
|
|
assert.Equal(t, *source.PermissionValues, *linked.PermissionValues)
|
|
assert.Equal(t, *source.PermissionOptions, *linked.PermissionOptions)
|
|
})
|
|
|
|
t.Run("linked field inherits source access_mode even if caller supplies different value", func(t *testing.T) {
|
|
source := createProtectedTemplate(t, "MismatchSource-"+model.NewId(), model.PropertyAccessModeSourceOnly)
|
|
|
|
linked, err := th.service.CreatePropertyField(rctxPlugin1, &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
Name: "Linked-" + model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
Attrs: model.StringInterface{
|
|
model.PropertyAttrsAccessMode: model.PropertyAccessModeSharedOnly,
|
|
},
|
|
LinkedFieldID: &source.ID,
|
|
})
|
|
require.NoError(t, err)
|
|
// Caller supplied shared_only but source's source_only is inherited
|
|
assert.Equal(t, model.PropertyAccessModeSourceOnly, linked.Attrs[model.PropertyAttrsAccessMode])
|
|
})
|
|
|
|
t.Run("linked field from unprotected template does not inherit protection", func(t *testing.T) {
|
|
source := createUnprotectedTemplate(t, "UnprotectedSource-"+model.NewId())
|
|
|
|
linked, err := th.service.CreatePropertyField(rctxPlugin1, &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
Name: "Linked-" + model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
LinkedFieldID: &source.ID,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
assert.False(t, model.IsPropertyFieldProtected(linked))
|
|
})
|
|
|
|
t.Run("non-plugin caller can create linked field from unprotected template", func(t *testing.T) {
|
|
source := createUnprotectedTemplate(t, "UnprotectedSource-"+model.NewId())
|
|
|
|
linked, err := th.service.CreatePropertyField(rctxUser, &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
Name: "Linked-" + model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
LinkedFieldID: &source.ID,
|
|
})
|
|
require.NoError(t, err)
|
|
assert.False(t, model.IsPropertyFieldProtected(linked))
|
|
})
|
|
}
|