mirror of
https://github.com/hanzoai/team.git
synced 2026-09-30 01:57:35 +00:00
Plugins are separate processes. They read "Mattermost-User-Id" off the request as a literal string, compiled against the published server/public module -- not against this tree. Renaming it here did not rename it there. It also produced a header name with a space in it, which is not a valid header name, so what shipped was a request the plugin could not read and would not have been able to read under any spelling. Every plugin HTTP route answered 401: the Agents API, its admin config, the webapp's whole agents surface. The same rename hit "Mattermost-Plugin-ID", the push proxy's "X-Mattermost-Auth" and "X-Mattermost-ServerID", and the session attribute device header. These are wire protocol, and the wire is shared with software that is not ours to rename. Brand strings the product actually shows -- the site name, the MFA issuer, email product names -- are untouched. The outbound user agent stays ours but loses its space: HanzoTeam-Bot/1.1 is a product token, "Hanzo Team-Bot/1.1" is two. Co-authored-by: Hanzo Dev <[email protected]>
1550 lines
52 KiB
Go
1550 lines
52 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package model
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestAccessPolicyVersionV0_1(t *testing.T) {
|
|
t.Run("invalid type", func(t *testing.T) {
|
|
policy := &AccessControlPolicy{
|
|
ID: "policy_id",
|
|
Type: "invalid_type",
|
|
Name: "Test Policy",
|
|
Revision: 1,
|
|
Version: AccessControlPolicyVersionV0_1,
|
|
Rules: []AccessControlPolicyRule{{Actions: []string{"read"}, Expression: "user.role == 'admin'"}},
|
|
}
|
|
|
|
err := policy.accessPolicyVersionV0_1()
|
|
require.NotNil(t, err, "Should return error for invalid type")
|
|
require.Equal(t, "model.access_policy.is_valid.type.app_error", err.Id)
|
|
})
|
|
|
|
t.Run("invalid ID", func(t *testing.T) {
|
|
policy := &AccessControlPolicy{
|
|
ID: "",
|
|
Type: AccessControlPolicyTypeParent,
|
|
Name: "Test Policy",
|
|
Revision: 1,
|
|
Version: AccessControlPolicyVersionV0_1,
|
|
Rules: []AccessControlPolicyRule{{Actions: []string{"read"}, Expression: "user.role == 'admin'"}},
|
|
}
|
|
|
|
err := policy.accessPolicyVersionV0_1()
|
|
require.NotNil(t, err, "Should return error for invalid ID")
|
|
require.Equal(t, "model.access_policy.is_valid.id.app_error", err.Id)
|
|
})
|
|
|
|
t.Run("parent policy with empty name", func(t *testing.T) {
|
|
policy := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeParent,
|
|
Name: "",
|
|
Revision: 1,
|
|
Version: AccessControlPolicyVersionV0_1,
|
|
Rules: []AccessControlPolicyRule{{Actions: []string{"read"}, Expression: "user.role == 'admin'"}},
|
|
}
|
|
|
|
err := policy.accessPolicyVersionV0_1()
|
|
require.NotNil(t, err, "Should return error for empty name in parent policy")
|
|
require.Equal(t, "model.access_policy.is_valid.name.app_error", err.Id)
|
|
})
|
|
|
|
t.Run("parent policy with too long name", func(t *testing.T) {
|
|
var longName strings.Builder
|
|
for i := 0; i <= MaxPolicyNameLength; i++ {
|
|
longName.WriteString("a")
|
|
}
|
|
|
|
policy := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeParent,
|
|
Name: longName.String(),
|
|
Revision: 1,
|
|
Version: AccessControlPolicyVersionV0_1,
|
|
Rules: []AccessControlPolicyRule{{Actions: []string{"read"}, Expression: "user.role == 'admin'"}},
|
|
}
|
|
|
|
err := policy.accessPolicyVersionV0_1()
|
|
require.NotNil(t, err, "Should return error for too long name in parent policy")
|
|
require.Equal(t, "model.access_policy.is_valid.name.app_error", err.Id)
|
|
})
|
|
|
|
t.Run("negative revision", func(t *testing.T) {
|
|
policy := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeParent,
|
|
Name: "Test Policy",
|
|
Revision: -1,
|
|
Version: AccessControlPolicyVersionV0_1,
|
|
Rules: []AccessControlPolicyRule{{Actions: []string{"read"}, Expression: "user.role == 'admin'"}},
|
|
}
|
|
|
|
err := policy.accessPolicyVersionV0_1()
|
|
require.NotNil(t, err, "Should return error for negative revision")
|
|
require.Equal(t, "model.access_policy.is_valid.revision.app_error", err.Id)
|
|
})
|
|
|
|
t.Run("invalid version", func(t *testing.T) {
|
|
policy := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeParent,
|
|
Name: "Test Policy",
|
|
Revision: 1,
|
|
Version: "invalid-version",
|
|
Rules: []AccessControlPolicyRule{{Actions: []string{"read"}, Expression: "user.role == 'admin'"}},
|
|
}
|
|
|
|
err := policy.accessPolicyVersionV0_1()
|
|
require.NotNil(t, err, "Should return error for invalid version")
|
|
require.Equal(t, "model.access_policy.is_valid.version.app_error", err.Id)
|
|
})
|
|
|
|
t.Run("parent policy with no rules", func(t *testing.T) {
|
|
policy := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeParent,
|
|
Name: "Test Policy",
|
|
Revision: 1,
|
|
Version: AccessControlPolicyVersionV0_1,
|
|
Rules: []AccessControlPolicyRule{},
|
|
}
|
|
|
|
err := policy.accessPolicyVersionV0_1()
|
|
require.NotNil(t, err, "Should return error for parent policy with no rules")
|
|
require.Equal(t, "model.access_policy.is_valid.rules.app_error", err.Id)
|
|
})
|
|
|
|
t.Run("parent policy with imports", func(t *testing.T) {
|
|
policy := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeParent,
|
|
Name: "Test Policy",
|
|
Revision: 1,
|
|
Version: AccessControlPolicyVersionV0_1,
|
|
Rules: []AccessControlPolicyRule{{Actions: []string{"read"}, Expression: "user.role == 'admin'"}},
|
|
Imports: []string{"some_import"},
|
|
}
|
|
|
|
err := policy.accessPolicyVersionV0_1()
|
|
require.NotNil(t, err, "Should return error for parent policy with imports")
|
|
require.Equal(t, "model.access_policy.is_valid.imports.app_error", err.Id)
|
|
})
|
|
|
|
t.Run("channel policy with no rules", func(t *testing.T) {
|
|
policy := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeChannel,
|
|
Name: "Test Policy",
|
|
Revision: 1,
|
|
Version: AccessControlPolicyVersionV0_1,
|
|
Rules: []AccessControlPolicyRule{},
|
|
Imports: []string{"parent_policy_id"},
|
|
}
|
|
|
|
err := policy.accessPolicyVersionV0_1()
|
|
require.NotNil(t, err, "Should return error for channel policy with no rules")
|
|
require.Equal(t, "model.access_policy.is_valid.rules.app_error", err.Id)
|
|
})
|
|
|
|
t.Run("channel policy with no imports", func(t *testing.T) {
|
|
policy := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeChannel,
|
|
Name: "Test Policy",
|
|
Revision: 1,
|
|
Version: AccessControlPolicyVersionV0_1,
|
|
Rules: []AccessControlPolicyRule{{Actions: []string{"read"}, Expression: "user.role == 'admin'"}},
|
|
Imports: []string{},
|
|
}
|
|
|
|
err := policy.accessPolicyVersionV0_1()
|
|
require.Nil(t, err, "Should not return error for channel policy with no imports")
|
|
})
|
|
|
|
t.Run("channel policy with multiple imports", func(t *testing.T) {
|
|
policy := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeChannel,
|
|
Name: "Test Policy",
|
|
Revision: 1,
|
|
Version: AccessControlPolicyVersionV0_1,
|
|
Rules: []AccessControlPolicyRule{{Actions: []string{"read"}, Expression: "user.role == 'admin'"}},
|
|
Imports: []string{"parent_policy_id1", "parent_policy_id2"},
|
|
}
|
|
|
|
err := policy.accessPolicyVersionV0_1()
|
|
require.NotNil(t, err, "Should return error for channel policy with multiple imports")
|
|
require.Equal(t, "model.access_policy.is_valid.imports.app_error", err.Id)
|
|
})
|
|
|
|
t.Run("valid parent policy", func(t *testing.T) {
|
|
policy := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeParent,
|
|
Name: "Test Policy",
|
|
Revision: 1,
|
|
Version: "v0.1",
|
|
Rules: []AccessControlPolicyRule{{Actions: []string{"read"}, Expression: "user.role == 'admin'"}},
|
|
}
|
|
|
|
err := policy.accessPolicyVersionV0_1()
|
|
require.Nil(t, err, "Should not return error for valid parent policy")
|
|
})
|
|
|
|
t.Run("valid channel policy", func(t *testing.T) {
|
|
policy := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeChannel,
|
|
Name: "Test Policy",
|
|
Revision: 1,
|
|
Version: "v0.1",
|
|
Rules: []AccessControlPolicyRule{{Actions: []string{"read"}, Expression: "user.role == 'admin'"}},
|
|
Imports: []string{"parent_policy_id"},
|
|
}
|
|
|
|
err := policy.accessPolicyVersionV0_1()
|
|
require.Nil(t, err, "Should not return error for valid channel policy")
|
|
})
|
|
}
|
|
|
|
func TestAccessControlPolicyValidateScope(t *testing.T) {
|
|
validPolicy := func() *AccessControlPolicy {
|
|
return &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeParent,
|
|
Name: "Test Policy",
|
|
Revision: 1,
|
|
Version: AccessControlPolicyVersionV0_1,
|
|
Rules: []AccessControlPolicyRule{{Actions: []string{"*"}, Expression: "true"}},
|
|
}
|
|
}
|
|
|
|
t.Run("no scope fields set — valid", func(t *testing.T) {
|
|
p := validPolicy()
|
|
require.Nil(t, p.IsValid())
|
|
})
|
|
|
|
t.Run("scope=team with valid scope_id — valid", func(t *testing.T) {
|
|
p := validPolicy()
|
|
p.Scope = AccessControlPolicyScopeTeam
|
|
p.ScopeID = NewId()
|
|
require.Nil(t, p.IsValid())
|
|
})
|
|
|
|
t.Run("scope_id set without scope — invalid", func(t *testing.T) {
|
|
p := validPolicy()
|
|
p.ScopeID = NewId()
|
|
err := p.IsValid()
|
|
require.NotNil(t, err)
|
|
require.Equal(t, "model.access_policy.is_valid.scope_id_without_scope.app_error", err.Id)
|
|
})
|
|
|
|
t.Run("scope=team with empty scope_id — invalid", func(t *testing.T) {
|
|
p := validPolicy()
|
|
p.Scope = AccessControlPolicyScopeTeam
|
|
p.ScopeID = ""
|
|
err := p.IsValid()
|
|
require.NotNil(t, err)
|
|
require.Equal(t, "model.access_policy.is_valid.scope_id.app_error", err.Id)
|
|
})
|
|
|
|
t.Run("unknown scope value — invalid", func(t *testing.T) {
|
|
p := validPolicy()
|
|
p.Scope = "unknown"
|
|
p.ScopeID = NewId()
|
|
err := p.IsValid()
|
|
require.NotNil(t, err)
|
|
require.Equal(t, "model.access_policy.is_valid.scope.app_error", err.Id)
|
|
})
|
|
}
|
|
|
|
func TestAccessPolicyVersionV0_3(t *testing.T) {
|
|
validRule := AccessControlPolicyRule{
|
|
Actions: []string{AccessControlPolicyActionMembership},
|
|
Expression: "user.properties.dept == \"eng\"",
|
|
}
|
|
|
|
t.Run("valid parent type", func(t *testing.T) {
|
|
policy := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeParent,
|
|
Name: "Parent",
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_3,
|
|
Rules: []AccessControlPolicyRule{{
|
|
Actions: []string{
|
|
AccessControlPolicyActionMembership,
|
|
AccessControlPolicyActionUploadFileAttachment,
|
|
AccessControlPolicyActionDownloadFileAttachment,
|
|
},
|
|
Expression: "user.properties.dept == \"eng\"",
|
|
}},
|
|
}
|
|
require.Nil(t, policy.accessPolicyVersionV0_3())
|
|
})
|
|
|
|
t.Run("valid channel type", func(t *testing.T) {
|
|
policy := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeChannel,
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_3,
|
|
Imports: []string{NewId()},
|
|
Rules: []AccessControlPolicyRule{validRule},
|
|
}
|
|
require.Nil(t, policy.accessPolicyVersionV0_3())
|
|
})
|
|
|
|
t.Run("valid permission type", func(t *testing.T) {
|
|
policy := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypePermission,
|
|
Name: "Permission",
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_3,
|
|
Roles: []string{"system_admin"},
|
|
Rules: []AccessControlPolicyRule{validRule},
|
|
}
|
|
require.Nil(t, policy.accessPolicyVersionV0_3())
|
|
})
|
|
|
|
t.Run("invalid type", func(t *testing.T) {
|
|
policy := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: "unknown",
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_3,
|
|
Rules: []AccessControlPolicyRule{validRule},
|
|
}
|
|
err := policy.accessPolicyVersionV0_3()
|
|
require.NotNil(t, err)
|
|
require.Equal(t, "model.access_policy.is_valid.type.app_error", err.Id)
|
|
})
|
|
|
|
t.Run("parent with no rules", func(t *testing.T) {
|
|
policy := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeParent,
|
|
Name: "Parent",
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_3,
|
|
Rules: []AccessControlPolicyRule{},
|
|
}
|
|
err := policy.accessPolicyVersionV0_3()
|
|
require.NotNil(t, err)
|
|
require.Equal(t, "model.access_policy.is_valid.rules.app_error", err.Id)
|
|
})
|
|
|
|
t.Run("parent with non-empty imports", func(t *testing.T) {
|
|
policy := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeParent,
|
|
Name: "Parent",
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_3,
|
|
Rules: []AccessControlPolicyRule{validRule},
|
|
Imports: []string{NewId()},
|
|
}
|
|
err := policy.accessPolicyVersionV0_3()
|
|
require.NotNil(t, err)
|
|
require.Equal(t, "model.access_policy.is_valid.imports.app_error", err.Id)
|
|
})
|
|
|
|
t.Run("permission with empty roles", func(t *testing.T) {
|
|
policy := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypePermission,
|
|
Name: "Permission",
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_3,
|
|
Roles: []string{},
|
|
Rules: []AccessControlPolicyRule{validRule},
|
|
}
|
|
err := policy.accessPolicyVersionV0_3()
|
|
require.NotNil(t, err)
|
|
require.Equal(t, "model.access_policy.is_valid.roles.app_error", err.Id)
|
|
})
|
|
|
|
t.Run("permission with blank role string", func(t *testing.T) {
|
|
policy := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypePermission,
|
|
Name: "Permission",
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_3,
|
|
Roles: []string{""},
|
|
Rules: []AccessControlPolicyRule{validRule},
|
|
}
|
|
err := policy.accessPolicyVersionV0_3()
|
|
require.NotNil(t, err)
|
|
require.Equal(t, "model.access_policy.is_valid.roles.app_error", err.Id)
|
|
})
|
|
|
|
t.Run("permission with multiple roles", func(t *testing.T) {
|
|
policy := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypePermission,
|
|
Name: "Permission",
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_3,
|
|
Roles: []string{"system_admin", "system_user"},
|
|
Rules: []AccessControlPolicyRule{validRule},
|
|
}
|
|
err := policy.accessPolicyVersionV0_3()
|
|
require.NotNil(t, err)
|
|
require.Equal(t, "model.access_policy.is_valid.roles.app_error", err.Id)
|
|
})
|
|
|
|
t.Run("permission with imports", func(t *testing.T) {
|
|
policy := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypePermission,
|
|
Name: "Permission",
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_3,
|
|
Roles: []string{"system_admin"},
|
|
Rules: []AccessControlPolicyRule{validRule},
|
|
Imports: []string{NewId()},
|
|
}
|
|
err := policy.accessPolicyVersionV0_3()
|
|
require.NotNil(t, err)
|
|
require.Equal(t, "model.access_policy.is_valid.imports.app_error", err.Id)
|
|
})
|
|
|
|
t.Run("permission with empty name", func(t *testing.T) {
|
|
policy := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypePermission,
|
|
Name: "",
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_3,
|
|
Roles: []string{"system_admin"},
|
|
Rules: []AccessControlPolicyRule{validRule},
|
|
}
|
|
err := policy.accessPolicyVersionV0_3()
|
|
require.NotNil(t, err)
|
|
require.Equal(t, "model.access_policy.is_valid.name.app_error", err.Id)
|
|
})
|
|
|
|
t.Run("permission with name exceeding max length", func(t *testing.T) {
|
|
policy := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypePermission,
|
|
Name: strings.Repeat("a", MaxPolicyNameLength+1),
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_3,
|
|
Roles: []string{"system_admin"},
|
|
Rules: []AccessControlPolicyRule{validRule},
|
|
}
|
|
err := policy.accessPolicyVersionV0_3()
|
|
require.NotNil(t, err)
|
|
require.Equal(t, "model.access_policy.is_valid.name.app_error", err.Id)
|
|
})
|
|
|
|
t.Run("unrecognized action", func(t *testing.T) {
|
|
policy := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeParent,
|
|
Name: "Parent",
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_3,
|
|
Rules: []AccessControlPolicyRule{{
|
|
Actions: []string{"not_a_real_action"},
|
|
Expression: "true",
|
|
}},
|
|
}
|
|
err := policy.accessPolicyVersionV0_3()
|
|
require.NotNil(t, err)
|
|
require.Equal(t, "model.access_policy.is_valid.actions.app_error", err.Id)
|
|
require.Contains(t, err.DetailedError, "not_a_real_action")
|
|
})
|
|
|
|
t.Run("empty actions slice", func(t *testing.T) {
|
|
policy := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeParent,
|
|
Name: "Parent",
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_3,
|
|
Rules: []AccessControlPolicyRule{{
|
|
Actions: []string{},
|
|
Expression: "true",
|
|
}},
|
|
}
|
|
err := policy.accessPolicyVersionV0_3()
|
|
require.NotNil(t, err)
|
|
require.Equal(t, "model.access_policy.is_valid.actions.app_error", err.Id)
|
|
})
|
|
|
|
t.Run("channel with no rules and no imports", func(t *testing.T) {
|
|
policy := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeChannel,
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_3,
|
|
}
|
|
err := policy.accessPolicyVersionV0_3()
|
|
require.NotNil(t, err)
|
|
require.Equal(t, "model.access_policy.is_valid.rules_imports.app_error", err.Id)
|
|
})
|
|
|
|
t.Run("membership rule rejects session attribute reference", func(t *testing.T) {
|
|
policy := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeParent,
|
|
Name: "Parent",
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_3,
|
|
Rules: []AccessControlPolicyRule{{
|
|
Actions: []string{AccessControlPolicyActionMembership},
|
|
Expression: "user.session.ip_address == \"10.0.0.1\"",
|
|
}},
|
|
}
|
|
err := policy.accessPolicyVersionV0_3()
|
|
require.NotNil(t, err)
|
|
require.Equal(t, "model.access_policy.is_valid.session_attribute_on_membership.app_error", err.Id)
|
|
})
|
|
|
|
t.Run("permission rule allows session attribute reference", func(t *testing.T) {
|
|
policy := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypePermission,
|
|
Name: "Permission",
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_3,
|
|
Roles: []string{"system_user"},
|
|
Rules: []AccessControlPolicyRule{{
|
|
Actions: []string{AccessControlPolicyActionUploadFileAttachment},
|
|
Expression: "user.session.ip_address == \"10.0.0.1\"",
|
|
}},
|
|
}
|
|
require.Nil(t, policy.accessPolicyVersionV0_3())
|
|
})
|
|
|
|
t.Run("mixed-action rule rejects session attribute reference", func(t *testing.T) {
|
|
policy := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeParent,
|
|
Name: "Parent",
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_3,
|
|
Rules: []AccessControlPolicyRule{{
|
|
Actions: []string{
|
|
AccessControlPolicyActionMembership,
|
|
AccessControlPolicyActionUploadFileAttachment,
|
|
},
|
|
Expression: "user.session.ip_address == \"10.0.0.1\"",
|
|
}},
|
|
}
|
|
err := policy.accessPolicyVersionV0_3()
|
|
require.NotNil(t, err)
|
|
require.Equal(t, "model.access_policy.is_valid.session_attribute_on_membership.app_error", err.Id)
|
|
})
|
|
|
|
t.Run("membership rule without session reference is accepted", func(t *testing.T) {
|
|
policy := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeParent,
|
|
Name: "Parent",
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_3,
|
|
Rules: []AccessControlPolicyRule{{
|
|
Actions: []string{AccessControlPolicyActionMembership},
|
|
Expression: "user.attributes.team == \"eng\"",
|
|
}},
|
|
}
|
|
require.Nil(t, policy.accessPolicyVersionV0_3())
|
|
})
|
|
|
|
t.Run("membership rule rejects user.session inside string literal (lexical check)", func(t *testing.T) {
|
|
policy := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeParent,
|
|
Name: "Parent",
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_3,
|
|
Rules: []AccessControlPolicyRule{{
|
|
Actions: []string{AccessControlPolicyActionMembership},
|
|
Expression: "user.attributes.note == \"see user.session for context\"",
|
|
}},
|
|
}
|
|
err := policy.accessPolicyVersionV0_3()
|
|
require.NotNil(t, err)
|
|
require.Equal(t, "model.access_policy.is_valid.session_attribute_on_membership.app_error", err.Id)
|
|
})
|
|
}
|
|
|
|
func TestAccessPolicyVersionV0_4(t *testing.T) {
|
|
validMembership := AccessControlPolicyRule{
|
|
Actions: []string{AccessControlPolicyActionMembership},
|
|
Expression: "user.attributes.dept == \"eng\"",
|
|
}
|
|
validPermission := func(name, role, action string) AccessControlPolicyRule {
|
|
return AccessControlPolicyRule{
|
|
Name: name,
|
|
Role: role,
|
|
Actions: []string{action},
|
|
Expression: "user.attributes.dept == \"eng\"",
|
|
}
|
|
}
|
|
|
|
t.Run("valid channel policy with membership and permission rules", func(t *testing.T) {
|
|
policy := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeChannel,
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_4,
|
|
Rules: []AccessControlPolicyRule{
|
|
validMembership,
|
|
validPermission("Block external uploads", ChannelUserRoleId, AccessControlPolicyActionUploadFileAttachment),
|
|
validPermission("Admin overrides", ChannelAdminRoleId, AccessControlPolicyActionDownloadFileAttachment),
|
|
},
|
|
}
|
|
require.Nil(t, policy.accessPolicyVersionV0_4())
|
|
})
|
|
|
|
t.Run("permission rule missing role rejected", func(t *testing.T) {
|
|
policy := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeChannel,
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_4,
|
|
Rules: []AccessControlPolicyRule{{
|
|
Name: "Block external uploads",
|
|
Actions: []string{AccessControlPolicyActionUploadFileAttachment},
|
|
Expression: "true",
|
|
}},
|
|
}
|
|
err := policy.accessPolicyVersionV0_4()
|
|
require.NotNil(t, err)
|
|
require.Equal(t, "model.access_policy.is_valid.rule_role.app_error", err.Id)
|
|
})
|
|
|
|
t.Run("permission rule with invalid role rejected", func(t *testing.T) {
|
|
policy := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeChannel,
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_4,
|
|
Rules: []AccessControlPolicyRule{{
|
|
Name: "Block external uploads",
|
|
Role: SystemUserRoleId, // wrong scope: must be channel role
|
|
Actions: []string{AccessControlPolicyActionUploadFileAttachment},
|
|
Expression: "true",
|
|
}},
|
|
}
|
|
err := policy.accessPolicyVersionV0_4()
|
|
require.NotNil(t, err)
|
|
require.Equal(t, "model.access_policy.is_valid.rule_role.app_error", err.Id)
|
|
})
|
|
|
|
t.Run("permission rule missing name rejected", func(t *testing.T) {
|
|
policy := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeChannel,
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_4,
|
|
Rules: []AccessControlPolicyRule{{
|
|
Role: ChannelUserRoleId,
|
|
Actions: []string{AccessControlPolicyActionUploadFileAttachment},
|
|
Expression: "true",
|
|
}},
|
|
}
|
|
err := policy.accessPolicyVersionV0_4()
|
|
require.NotNil(t, err)
|
|
require.Equal(t, "model.access_policy.is_valid.rule_name.app_error", err.Id)
|
|
})
|
|
|
|
t.Run("duplicate permission rule names rejected", func(t *testing.T) {
|
|
policy := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeChannel,
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_4,
|
|
Rules: []AccessControlPolicyRule{
|
|
validPermission("Block uploads", ChannelUserRoleId, AccessControlPolicyActionUploadFileAttachment),
|
|
validPermission("Block uploads", ChannelAdminRoleId, AccessControlPolicyActionDownloadFileAttachment),
|
|
},
|
|
}
|
|
err := policy.accessPolicyVersionV0_4()
|
|
require.NotNil(t, err)
|
|
require.Equal(t, "model.access_policy.is_valid.rule_name_unique.app_error", err.Id)
|
|
})
|
|
|
|
t.Run("membership combined with permission action rejected", func(t *testing.T) {
|
|
policy := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeChannel,
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_4,
|
|
Rules: []AccessControlPolicyRule{{
|
|
Name: "Combined",
|
|
Role: ChannelUserRoleId,
|
|
Actions: []string{AccessControlPolicyActionMembership, AccessControlPolicyActionUploadFileAttachment},
|
|
Expression: "true",
|
|
}},
|
|
}
|
|
err := policy.accessPolicyVersionV0_4()
|
|
require.NotNil(t, err)
|
|
require.Equal(t, "model.access_policy.is_valid.actions.membership_combined.app_error", err.Id)
|
|
})
|
|
|
|
t.Run("membership rule with role rejected", func(t *testing.T) {
|
|
policy := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeChannel,
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_4,
|
|
Rules: []AccessControlPolicyRule{{
|
|
Role: ChannelUserRoleId,
|
|
Actions: []string{AccessControlPolicyActionMembership},
|
|
Expression: "true",
|
|
}},
|
|
}
|
|
err := policy.accessPolicyVersionV0_4()
|
|
require.NotNil(t, err)
|
|
require.Equal(t, "model.access_policy.is_valid.rule_role.app_error", err.Id)
|
|
})
|
|
|
|
t.Run("permission rule on parent policy rejected", func(t *testing.T) {
|
|
policy := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeParent,
|
|
Name: "Parent",
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_4,
|
|
Rules: []AccessControlPolicyRule{
|
|
validPermission("Block uploads", ChannelUserRoleId, AccessControlPolicyActionUploadFileAttachment),
|
|
},
|
|
}
|
|
err := policy.accessPolicyVersionV0_4()
|
|
require.NotNil(t, err)
|
|
require.Equal(t, "model.access_policy.is_valid.actions.permission_type.app_error", err.Id)
|
|
})
|
|
}
|
|
|
|
func TestInheritV0_4(t *testing.T) {
|
|
t.Run("v0.4 child can import v0.4 parent", func(t *testing.T) {
|
|
// Same-version happy path: a v0.4 channel policy importing
|
|
// another v0.4 parent should be accepted (Inherit only blocks
|
|
// v0.4 children importing pre-v0.3 parents).
|
|
parentID := NewId()
|
|
parent := &AccessControlPolicy{
|
|
ID: parentID,
|
|
Type: AccessControlPolicyTypeParent,
|
|
Name: "Parent V04",
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_4,
|
|
Rules: []AccessControlPolicyRule{{
|
|
Actions: []string{AccessControlPolicyActionMembership},
|
|
Expression: "true",
|
|
}},
|
|
}
|
|
child := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeChannel,
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_4,
|
|
Rules: []AccessControlPolicyRule{{
|
|
Actions: []string{AccessControlPolicyActionMembership},
|
|
Expression: "true",
|
|
}},
|
|
}
|
|
|
|
err := child.Inherit(parent)
|
|
require.Nil(t, err)
|
|
require.Contains(t, child.Imports, parentID)
|
|
})
|
|
|
|
t.Run("v0.4 child can import v0.3 parent", func(t *testing.T) {
|
|
parentID := NewId()
|
|
parent := &AccessControlPolicy{
|
|
ID: parentID,
|
|
Type: AccessControlPolicyTypeParent,
|
|
Name: "Parent",
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_3,
|
|
Rules: []AccessControlPolicyRule{{
|
|
Actions: []string{AccessControlPolicyActionMembership},
|
|
Expression: "true",
|
|
}},
|
|
}
|
|
child := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeChannel,
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_4,
|
|
Rules: []AccessControlPolicyRule{{
|
|
Actions: []string{AccessControlPolicyActionMembership},
|
|
Expression: "true",
|
|
}},
|
|
}
|
|
|
|
err := child.Inherit(parent)
|
|
require.Nil(t, err)
|
|
require.Contains(t, child.Imports, parentID)
|
|
})
|
|
|
|
t.Run("v0.4 child cannot import v0.1 parent", func(t *testing.T) {
|
|
parent := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeParent,
|
|
Name: "V01 Parent",
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_1,
|
|
Rules: []AccessControlPolicyRule{{
|
|
Actions: []string{"read"},
|
|
Expression: "true",
|
|
}},
|
|
}
|
|
child := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeChannel,
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_4,
|
|
Rules: []AccessControlPolicyRule{{
|
|
Actions: []string{AccessControlPolicyActionMembership},
|
|
Expression: "true",
|
|
}},
|
|
}
|
|
|
|
err := child.Inherit(parent)
|
|
require.NotNil(t, err)
|
|
require.Equal(t, "model.access_policy.inherit.version.app_error", err.Id)
|
|
})
|
|
|
|
t.Run("v0.4 child rejects permission-type parent", func(t *testing.T) {
|
|
parent := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypePermission,
|
|
Name: "Permission",
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_3,
|
|
Roles: []string{"system_admin"},
|
|
Rules: []AccessControlPolicyRule{{
|
|
Actions: []string{AccessControlPolicyActionMembership},
|
|
Expression: "true",
|
|
}},
|
|
}
|
|
child := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeChannel,
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_4,
|
|
Rules: []AccessControlPolicyRule{{
|
|
Actions: []string{AccessControlPolicyActionMembership},
|
|
Expression: "true",
|
|
}},
|
|
}
|
|
|
|
err := child.Inherit(parent)
|
|
require.NotNil(t, err)
|
|
require.Equal(t, "model.access_policy.inherit.permission.app_error", err.Id)
|
|
})
|
|
|
|
// v0.4 imports are strictly child-channel → parent-membership.
|
|
// A channel→channel import would write a peer channel policy's ID
|
|
// into Imports where the loader expects a membership parent — the
|
|
// resulting evaluation would silently misroute. Reject up front.
|
|
t.Run("v0.4 child rejects channel-type parent", func(t *testing.T) {
|
|
parent := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeChannel,
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_4,
|
|
Rules: []AccessControlPolicyRule{{
|
|
Actions: []string{AccessControlPolicyActionMembership},
|
|
Expression: "true",
|
|
}},
|
|
}
|
|
child := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeChannel,
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_4,
|
|
Rules: []AccessControlPolicyRule{{
|
|
Actions: []string{AccessControlPolicyActionMembership},
|
|
Expression: "true",
|
|
}},
|
|
}
|
|
|
|
err := child.Inherit(parent)
|
|
require.NotNil(t, err)
|
|
require.Equal(t, "model.access_policy.inherit.parent_type.app_error", err.Id)
|
|
require.Empty(t, child.Imports, "rejected imports must not leak into the child's Imports slice")
|
|
})
|
|
}
|
|
|
|
func TestSubjectRoleForScope(t *testing.T) {
|
|
t.Run("scoped roles take precedence", func(t *testing.T) {
|
|
s := &Subject{
|
|
Role: SystemUserRoleId, // legacy field
|
|
ScopedRoles: []ScopedRole{
|
|
{Scope: AccessControlSubjectScopeSystem, Role: SystemAdminRoleId},
|
|
{Scope: AccessControlSubjectScopeChannel, Role: ChannelAdminRoleId},
|
|
},
|
|
}
|
|
require.Equal(t, SystemAdminRoleId, s.RoleForScope(AccessControlSubjectScopeSystem))
|
|
require.Equal(t, ChannelAdminRoleId, s.RoleForScope(AccessControlSubjectScopeChannel))
|
|
})
|
|
|
|
t.Run("falls back to legacy Role for system scope when ScopedRoles empty", func(t *testing.T) {
|
|
s := &Subject{Role: SystemAdminRoleId}
|
|
require.Equal(t, SystemAdminRoleId, s.RoleForScope(AccessControlSubjectScopeSystem))
|
|
require.Equal(t, "", s.RoleForScope(AccessControlSubjectScopeChannel))
|
|
})
|
|
|
|
t.Run("returns empty for unknown scope", func(t *testing.T) {
|
|
s := &Subject{}
|
|
require.Equal(t, "", s.RoleForScope("unknown"))
|
|
})
|
|
}
|
|
|
|
func TestSubjectRolesForScope(t *testing.T) {
|
|
t.Run("returns every entry matching the scope in order", func(t *testing.T) {
|
|
s := &Subject{
|
|
ScopedRoles: []ScopedRole{
|
|
{Scope: AccessControlSubjectScopeSystem, Role: SystemUserRoleId},
|
|
{Scope: AccessControlSubjectScopeChannel, Role: ChannelAdminRoleId},
|
|
{Scope: AccessControlSubjectScopeSystem, Role: SystemAdminRoleId},
|
|
},
|
|
}
|
|
require.Equal(t, []string{SystemUserRoleId, SystemAdminRoleId}, s.RolesForScope(AccessControlSubjectScopeSystem))
|
|
require.Equal(t, []string{ChannelAdminRoleId}, s.RolesForScope(AccessControlSubjectScopeChannel))
|
|
})
|
|
|
|
t.Run("returns nil when no entry matches", func(t *testing.T) {
|
|
s := &Subject{
|
|
ScopedRoles: []ScopedRole{
|
|
{Scope: AccessControlSubjectScopeSystem, Role: SystemUserRoleId},
|
|
},
|
|
}
|
|
require.Nil(t, s.RolesForScope(AccessControlSubjectScopeChannel))
|
|
})
|
|
|
|
t.Run("does NOT fall back to legacy Role for system scope", func(t *testing.T) {
|
|
s := &Subject{Role: SystemAdminRoleId}
|
|
require.Nil(t, s.RolesForScope(AccessControlSubjectScopeSystem))
|
|
})
|
|
}
|
|
|
|
func TestSubjectSetScopedRole(t *testing.T) {
|
|
t.Run("appends when scope is absent", func(t *testing.T) {
|
|
s := &Subject{}
|
|
s.SetScopedRole(AccessControlSubjectScopeSystem, SystemUserRoleId)
|
|
require.Equal(t, []ScopedRole{
|
|
{Scope: AccessControlSubjectScopeSystem, Role: SystemUserRoleId},
|
|
}, s.ScopedRoles)
|
|
})
|
|
|
|
t.Run("replaces in place when scope already exists", func(t *testing.T) {
|
|
s := &Subject{
|
|
ScopedRoles: []ScopedRole{
|
|
{Scope: AccessControlSubjectScopeSystem, Role: SystemUserRoleId},
|
|
{Scope: AccessControlSubjectScopeChannel, Role: ChannelUserRoleId},
|
|
},
|
|
}
|
|
s.SetScopedRole(AccessControlSubjectScopeSystem, SystemAdminRoleId)
|
|
require.Equal(t, []ScopedRole{
|
|
{Scope: AccessControlSubjectScopeSystem, Role: SystemAdminRoleId},
|
|
{Scope: AccessControlSubjectScopeChannel, Role: ChannelUserRoleId},
|
|
}, s.ScopedRoles)
|
|
})
|
|
|
|
t.Run("collapses duplicate scope entries to one", func(t *testing.T) {
|
|
s := &Subject{
|
|
ScopedRoles: []ScopedRole{
|
|
{Scope: AccessControlSubjectScopeSystem, Role: SystemUserRoleId},
|
|
{Scope: AccessControlSubjectScopeChannel, Role: ChannelUserRoleId},
|
|
{Scope: AccessControlSubjectScopeSystem, Role: SystemGuestRoleId},
|
|
},
|
|
}
|
|
s.SetScopedRole(AccessControlSubjectScopeSystem, SystemAdminRoleId)
|
|
require.Equal(t, []ScopedRole{
|
|
{Scope: AccessControlSubjectScopeSystem, Role: SystemAdminRoleId},
|
|
{Scope: AccessControlSubjectScopeChannel, Role: ChannelUserRoleId},
|
|
}, s.ScopedRoles)
|
|
})
|
|
|
|
t.Run("empty role removes every entry for the scope", func(t *testing.T) {
|
|
s := &Subject{
|
|
ScopedRoles: []ScopedRole{
|
|
{Scope: AccessControlSubjectScopeSystem, Role: SystemUserRoleId},
|
|
{Scope: AccessControlSubjectScopeChannel, Role: ChannelUserRoleId},
|
|
{Scope: AccessControlSubjectScopeSystem, Role: SystemGuestRoleId},
|
|
},
|
|
}
|
|
s.SetScopedRole(AccessControlSubjectScopeSystem, "")
|
|
require.Equal(t, []ScopedRole{
|
|
{Scope: AccessControlSubjectScopeChannel, Role: ChannelUserRoleId},
|
|
}, s.ScopedRoles)
|
|
})
|
|
|
|
t.Run("empty role on absent scope is a no-op", func(t *testing.T) {
|
|
s := &Subject{
|
|
ScopedRoles: []ScopedRole{
|
|
{Scope: AccessControlSubjectScopeSystem, Role: SystemUserRoleId},
|
|
},
|
|
}
|
|
s.SetScopedRole(AccessControlSubjectScopeChannel, "")
|
|
require.Equal(t, []ScopedRole{
|
|
{Scope: AccessControlSubjectScopeSystem, Role: SystemUserRoleId},
|
|
}, s.ScopedRoles)
|
|
})
|
|
|
|
t.Run("empty scope is a no-op", func(t *testing.T) {
|
|
original := []ScopedRole{
|
|
{Scope: AccessControlSubjectScopeSystem, Role: SystemUserRoleId},
|
|
}
|
|
s := &Subject{ScopedRoles: original}
|
|
s.SetScopedRole("", SystemAdminRoleId)
|
|
require.Equal(t, original, s.ScopedRoles)
|
|
})
|
|
|
|
t.Run("does not mutate aliased backing array", func(t *testing.T) {
|
|
// Mirrors the attachChannelScopedRole hot path: a cached Subject is
|
|
// passed by value, its ScopedRoles slice header is copied but the
|
|
// backing array is shared. SetScopedRole must allocate a fresh array
|
|
// so the cached Subject's ScopedRoles is not corrupted.
|
|
cached := Subject{
|
|
ScopedRoles: []ScopedRole{
|
|
{Scope: AccessControlSubjectScopeSystem, Role: SystemUserRoleId},
|
|
},
|
|
}
|
|
copyOfCached := cached
|
|
copyOfCached.SetScopedRole(AccessControlSubjectScopeChannel, ChannelAdminRoleId)
|
|
require.Equal(t, []ScopedRole{
|
|
{Scope: AccessControlSubjectScopeSystem, Role: SystemUserRoleId},
|
|
}, cached.ScopedRoles, "cached Subject's ScopedRoles must not be mutated")
|
|
require.Equal(t, []ScopedRole{
|
|
{Scope: AccessControlSubjectScopeSystem, Role: SystemUserRoleId},
|
|
{Scope: AccessControlSubjectScopeChannel, Role: ChannelAdminRoleId},
|
|
}, copyOfCached.ScopedRoles)
|
|
})
|
|
}
|
|
|
|
func TestInheritV0_3(t *testing.T) {
|
|
t.Run("successful inherit", func(t *testing.T) {
|
|
parentID := NewId()
|
|
parent := &AccessControlPolicy{
|
|
ID: parentID,
|
|
Type: AccessControlPolicyTypeParent,
|
|
Name: "Parent",
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_3,
|
|
Rules: []AccessControlPolicyRule{{
|
|
Actions: []string{AccessControlPolicyActionMembership},
|
|
Expression: "true",
|
|
}},
|
|
}
|
|
child := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeChannel,
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_3,
|
|
Rules: []AccessControlPolicyRule{{
|
|
Actions: []string{AccessControlPolicyActionMembership},
|
|
Expression: "true",
|
|
}},
|
|
}
|
|
|
|
err := child.Inherit(parent)
|
|
require.Nil(t, err)
|
|
require.Contains(t, child.Imports, parentID)
|
|
})
|
|
|
|
t.Run("duplicate import guard", func(t *testing.T) {
|
|
parentID := NewId()
|
|
parent := &AccessControlPolicy{
|
|
ID: parentID,
|
|
Type: AccessControlPolicyTypeParent,
|
|
Name: "Parent",
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_3,
|
|
Rules: []AccessControlPolicyRule{{
|
|
Actions: []string{AccessControlPolicyActionMembership},
|
|
Expression: "true",
|
|
}},
|
|
}
|
|
child := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeChannel,
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_3,
|
|
Imports: []string{parentID},
|
|
Rules: []AccessControlPolicyRule{{
|
|
Actions: []string{AccessControlPolicyActionMembership},
|
|
Expression: "true",
|
|
}},
|
|
}
|
|
|
|
err := child.Inherit(parent)
|
|
require.NotNil(t, err)
|
|
require.Equal(t, "model.access_policy.inherit.already_imported.app_error", err.Id)
|
|
})
|
|
|
|
t.Run("permission type child rejected", func(t *testing.T) {
|
|
parent := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeParent,
|
|
Name: "Parent",
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_3,
|
|
Rules: []AccessControlPolicyRule{{
|
|
Actions: []string{AccessControlPolicyActionMembership},
|
|
Expression: "true",
|
|
}},
|
|
}
|
|
child := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypePermission,
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_3,
|
|
Roles: []string{"system_admin"},
|
|
Rules: []AccessControlPolicyRule{{
|
|
Actions: []string{AccessControlPolicyActionMembership},
|
|
Expression: "true",
|
|
}},
|
|
}
|
|
|
|
err := child.Inherit(parent)
|
|
require.NotNil(t, err)
|
|
require.Equal(t, "model.access_policy.inherit.permission.app_error", err.Id)
|
|
})
|
|
|
|
t.Run("permission type parent rejected", func(t *testing.T) {
|
|
parent := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypePermission,
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_3,
|
|
Roles: []string{"system_admin"},
|
|
Rules: []AccessControlPolicyRule{{
|
|
Actions: []string{AccessControlPolicyActionMembership},
|
|
Expression: "true",
|
|
}},
|
|
}
|
|
child := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeChannel,
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_3,
|
|
Rules: []AccessControlPolicyRule{{
|
|
Actions: []string{AccessControlPolicyActionMembership},
|
|
Expression: "true",
|
|
}},
|
|
}
|
|
|
|
err := child.Inherit(parent)
|
|
require.NotNil(t, err)
|
|
require.Equal(t, "model.access_policy.inherit.permission.app_error", err.Id)
|
|
})
|
|
|
|
t.Run("non-v0.3 parent version rejected", func(t *testing.T) {
|
|
parent := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeParent,
|
|
Name: "V01 Parent",
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_1,
|
|
Rules: []AccessControlPolicyRule{{
|
|
Actions: []string{"read"},
|
|
Expression: "true",
|
|
}},
|
|
}
|
|
child := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeChannel,
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_3,
|
|
Rules: []AccessControlPolicyRule{{
|
|
Actions: []string{AccessControlPolicyActionMembership},
|
|
Expression: "true",
|
|
}},
|
|
}
|
|
|
|
err := child.Inherit(parent)
|
|
require.NotNil(t, err)
|
|
require.Equal(t, "model.access_policy.inherit.version.app_error", err.Id)
|
|
})
|
|
}
|
|
|
|
func TestIsValidTeamType(t *testing.T) {
|
|
validID := NewId()
|
|
|
|
t.Run("team type with rules only is valid", func(t *testing.T) {
|
|
p := &AccessControlPolicy{
|
|
ID: validID,
|
|
Type: AccessControlPolicyTypeTeam,
|
|
Version: AccessControlPolicyVersionV0_3,
|
|
Revision: 0,
|
|
Rules: []AccessControlPolicyRule{{
|
|
Actions: []string{AccessControlPolicyActionMembership},
|
|
Expression: "true",
|
|
}},
|
|
}
|
|
require.Nil(t, p.IsValid())
|
|
})
|
|
|
|
t.Run("team type with imports only is valid", func(t *testing.T) {
|
|
p := &AccessControlPolicy{
|
|
ID: validID,
|
|
Type: AccessControlPolicyTypeTeam,
|
|
Version: AccessControlPolicyVersionV0_3,
|
|
Revision: 0,
|
|
Imports: []string{NewId()},
|
|
}
|
|
require.Nil(t, p.IsValid())
|
|
})
|
|
|
|
t.Run("team type with neither rules nor imports is invalid", func(t *testing.T) {
|
|
p := &AccessControlPolicy{
|
|
ID: validID,
|
|
Type: AccessControlPolicyTypeTeam,
|
|
Version: AccessControlPolicyVersionV0_3,
|
|
Revision: 0,
|
|
}
|
|
appErr := p.IsValid()
|
|
require.NotNil(t, appErr)
|
|
require.Equal(t, "model.access_policy.is_valid.rules_imports.app_error", appErr.Id)
|
|
})
|
|
}
|
|
|
|
func TestInheritTeamType(t *testing.T) {
|
|
makeParent := func() *AccessControlPolicy {
|
|
return &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeParent,
|
|
Name: "TestParent",
|
|
Version: AccessControlPolicyVersionV0_3,
|
|
Revision: 0,
|
|
Rules: []AccessControlPolicyRule{{
|
|
Actions: []string{AccessControlPolicyActionMembership},
|
|
Expression: "true",
|
|
}},
|
|
}
|
|
}
|
|
|
|
t.Run("team child inherits from parent is ok", func(t *testing.T) {
|
|
parent := makeParent()
|
|
child := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeTeam,
|
|
Version: AccessControlPolicyVersionV0_3,
|
|
Revision: 0,
|
|
Rules: []AccessControlPolicyRule{{
|
|
Actions: []string{AccessControlPolicyActionMembership},
|
|
Expression: "true",
|
|
}},
|
|
}
|
|
err := child.Inherit(parent)
|
|
require.Nil(t, err)
|
|
require.Contains(t, child.Imports, parent.ID)
|
|
})
|
|
|
|
t.Run("team child inheriting from team policy returns 400", func(t *testing.T) {
|
|
teamParent := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeTeam,
|
|
Version: AccessControlPolicyVersionV0_3,
|
|
Revision: 0,
|
|
Imports: []string{NewId()},
|
|
}
|
|
child := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeTeam,
|
|
Version: AccessControlPolicyVersionV0_3,
|
|
Revision: 0,
|
|
Rules: []AccessControlPolicyRule{{
|
|
Actions: []string{AccessControlPolicyActionMembership},
|
|
Expression: "true",
|
|
}},
|
|
}
|
|
err := child.Inherit(teamParent)
|
|
require.NotNil(t, err)
|
|
require.Equal(t, 400, err.StatusCode)
|
|
})
|
|
|
|
t.Run("team child inheriting from permission policy returns 400", func(t *testing.T) {
|
|
permParent := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypePermission,
|
|
Name: "PermParent",
|
|
Version: AccessControlPolicyVersionV0_3,
|
|
Revision: 0,
|
|
Roles: []string{"system_admin"},
|
|
Rules: []AccessControlPolicyRule{{
|
|
Actions: []string{AccessControlPolicyActionMembership},
|
|
Expression: "true",
|
|
}},
|
|
}
|
|
child := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeTeam,
|
|
Version: AccessControlPolicyVersionV0_3,
|
|
Revision: 0,
|
|
Rules: []AccessControlPolicyRule{{
|
|
Actions: []string{AccessControlPolicyActionMembership},
|
|
Expression: "true",
|
|
}},
|
|
}
|
|
err := child.Inherit(permParent)
|
|
require.NotNil(t, err)
|
|
require.Equal(t, 400, err.StatusCode)
|
|
})
|
|
}
|
|
|
|
// testPluginPolicyType is a well-formed plugin-owned policy type: the owning
|
|
// plugin ID, a colon, then the plugin's own resource-type name.
|
|
const testPluginPolicyType = "mattermost-ai:agent"
|
|
|
|
func TestAccessPolicyVersionV0_5(t *testing.T) {
|
|
validPolicy := func(mutate func(p *AccessControlPolicy)) *AccessControlPolicy {
|
|
p := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: testPluginPolicyType,
|
|
Name: "Agent policy",
|
|
Revision: 0,
|
|
Version: AccessControlPolicyVersionV0_5,
|
|
Rules: []AccessControlPolicyRule{{
|
|
Actions: []string{"use"},
|
|
Expression: "user.attributes.dept == \"eng\"",
|
|
}},
|
|
}
|
|
if mutate != nil {
|
|
mutate(p)
|
|
}
|
|
return p
|
|
}
|
|
|
|
for _, pluginType := range []string{
|
|
"mattermost-ai:agent",
|
|
"mattermost-ai:service",
|
|
"mattermost-ai:mcp",
|
|
"com.example.plugin:widget",
|
|
} {
|
|
t.Run("valid policy for "+pluginType, func(t *testing.T) {
|
|
p := validPolicy(func(p *AccessControlPolicy) { p.Type = pluginType })
|
|
require.Nil(t, p.IsValid())
|
|
})
|
|
}
|
|
|
|
t.Run("plugin-defined action accepted", func(t *testing.T) {
|
|
p := validPolicy(func(p *AccessControlPolicy) { p.Rules[0].Actions = []string{"invoke_tool"} })
|
|
require.Nil(t, p.IsValid())
|
|
})
|
|
|
|
t.Run("optional rule name accepted", func(t *testing.T) {
|
|
p := validPolicy(func(p *AccessControlPolicy) { p.Rules[0].Name = "Named rule" })
|
|
require.Nil(t, p.IsValid())
|
|
})
|
|
|
|
tests := []struct {
|
|
name string
|
|
mutate func(p *AccessControlPolicy)
|
|
expectedID string
|
|
}{
|
|
{"legacy channel type rejected", func(p *AccessControlPolicy) { p.Type = AccessControlPolicyTypeChannel }, "model.access_policy.is_valid.plugin_type.app_error"},
|
|
{"legacy parent type rejected", func(p *AccessControlPolicy) { p.Type = AccessControlPolicyTypeParent }, "model.access_policy.is_valid.plugin_type.app_error"},
|
|
{"legacy permission type rejected", func(p *AccessControlPolicy) { p.Type = AccessControlPolicyTypePermission }, "model.access_policy.is_valid.plugin_type.app_error"},
|
|
{"legacy team type rejected", func(p *AccessControlPolicy) { p.Type = AccessControlPolicyTypeTeam }, "model.access_policy.is_valid.plugin_type.app_error"},
|
|
{"unprefixed type rejected", func(p *AccessControlPolicy) { p.Type = "some-plugin.widget" }, "model.access_policy.is_valid.plugin_type.app_error"},
|
|
{"empty resource-type segment rejected", func(p *AccessControlPolicy) { p.Type = "mattermost-ai:" }, "model.access_policy.is_valid.plugin_type.app_error"},
|
|
{"empty plugin-ID segment rejected", func(p *AccessControlPolicy) { p.Type = ":agent" }, "model.access_policy.is_valid.plugin_type.app_error"},
|
|
{"resource-type segment with a separator rejected", func(p *AccessControlPolicy) { p.Type = "mattermost-ai:agent:v2" }, "model.access_policy.is_valid.plugin_type.app_error"},
|
|
{"oversized resource-type segment rejected", func(p *AccessControlPolicy) {
|
|
p.Type = "mattermost-ai:" + strings.Repeat("a", MaxPluginResourceTypeLength+1)
|
|
}, "model.access_policy.is_valid.plugin_type.app_error"},
|
|
{"invalid id", func(p *AccessControlPolicy) { p.ID = "short" }, "model.access_policy.is_valid.id.app_error"},
|
|
{"empty name", func(p *AccessControlPolicy) { p.Name = "" }, "model.access_policy.is_valid.name.app_error"},
|
|
{"oversized name", func(p *AccessControlPolicy) { p.Name = strings.Repeat("a", MaxPolicyNameLength+1) }, "model.access_policy.is_valid.name.app_error"},
|
|
{"negative revision", func(p *AccessControlPolicy) { p.Revision = -1 }, "model.access_policy.is_valid.revision.app_error"},
|
|
{"zero rules", func(p *AccessControlPolicy) { p.Rules = nil }, "model.access_policy.is_valid.rules.app_error"},
|
|
{"imports present", func(p *AccessControlPolicy) { p.Imports = []string{NewId()} }, "model.access_policy.is_valid.imports.app_error"},
|
|
{"roles present", func(p *AccessControlPolicy) { p.Roles = []string{SystemUserRoleId} }, "model.access_policy.is_valid.roles.app_error"},
|
|
{"empty actions", func(p *AccessControlPolicy) { p.Rules[0].Actions = nil }, "model.access_policy.is_valid.actions.app_error"},
|
|
{"wildcard action rejected", func(p *AccessControlPolicy) { p.Rules[0].Actions = []string{"*"} }, "model.access_policy.is_valid.actions.app_error"},
|
|
{"malformed action rejected", func(p *AccessControlPolicy) { p.Rules[0].Actions = []string{"Use It"} }, "model.access_policy.is_valid.actions.app_error"},
|
|
{"rule role rejected", func(p *AccessControlPolicy) { p.Rules[0].Role = ChannelUserRoleId }, "model.access_policy.is_valid.rule_role.app_error"},
|
|
{"oversized rule name", func(p *AccessControlPolicy) { p.Rules[0].Name = strings.Repeat("a", MaxPolicyNameLength+1) }, "model.access_policy.is_valid.rule_name.app_error"},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
p := validPolicy(tc.mutate)
|
|
err := p.IsValid()
|
|
require.NotNil(t, err)
|
|
require.Equal(t, tc.expectedID, err.Id)
|
|
})
|
|
}
|
|
|
|
// Scope rejections go through the top-level IsValid because
|
|
// validateScope runs first for non-empty scopes.
|
|
t.Run("team scope rejected", func(t *testing.T) {
|
|
p := validPolicy(func(p *AccessControlPolicy) {
|
|
p.Scope = AccessControlPolicyScopeTeam
|
|
p.ScopeID = NewId()
|
|
})
|
|
err := p.IsValid()
|
|
require.NotNil(t, err)
|
|
require.Equal(t, "model.access_policy.is_valid.scope.app_error", err.Id)
|
|
})
|
|
|
|
t.Run("scope id without scope rejected", func(t *testing.T) {
|
|
p := validPolicy(func(p *AccessControlPolicy) { p.ScopeID = NewId() })
|
|
err := p.IsValid()
|
|
require.NotNil(t, err)
|
|
require.Equal(t, "model.access_policy.is_valid.scope_id_without_scope.app_error", err.Id)
|
|
})
|
|
|
|
t.Run("invalid semver rejected", func(t *testing.T) {
|
|
// A bad semver can't reach the v0.5 validator through the IsValid
|
|
// switch, so exercise the validator directly.
|
|
p := validPolicy(nil)
|
|
p.Version = "not-semver"
|
|
err := p.accessPolicyVersionV0_5()
|
|
require.NotNil(t, err)
|
|
require.Equal(t, "model.access_policy.is_valid.version.app_error", err.Id)
|
|
})
|
|
}
|
|
|
|
func TestSplitPluginAccessControlPolicyType(t *testing.T) {
|
|
valid := []struct {
|
|
policyType string
|
|
pluginID string
|
|
resourceType string
|
|
}{
|
|
{"mattermost-ai:agent", "mattermost-ai", "agent"},
|
|
{"com.example.plugin:widget", "com.example.plugin", "widget"},
|
|
{"my_plugin:some.nested-type", "my_plugin", "some.nested-type"},
|
|
{"mattermost-ai:" + strings.Repeat("a", MaxPluginResourceTypeLength), "mattermost-ai", strings.Repeat("a", MaxPluginResourceTypeLength)},
|
|
// Exactly the column width: a 63-char plugin ID + ":" + 64-char type.
|
|
{
|
|
strings.Repeat("p", MaxPolicyTypeLength-MaxPluginResourceTypeLength-1) + ":" + strings.Repeat("a", MaxPluginResourceTypeLength),
|
|
strings.Repeat("p", MaxPolicyTypeLength-MaxPluginResourceTypeLength-1),
|
|
strings.Repeat("a", MaxPluginResourceTypeLength),
|
|
},
|
|
}
|
|
for _, tc := range valid {
|
|
t.Run("valid "+tc.policyType, func(t *testing.T) {
|
|
pluginID, resourceType, ok := SplitPluginAccessControlPolicyType(tc.policyType)
|
|
require.True(t, ok)
|
|
require.Equal(t, tc.pluginID, pluginID)
|
|
require.Equal(t, tc.resourceType, resourceType)
|
|
require.True(t, IsPluginAccessControlPolicyType(tc.policyType))
|
|
})
|
|
}
|
|
|
|
invalid := []string{
|
|
AccessControlPolicyTypeParent,
|
|
AccessControlPolicyTypeChannel,
|
|
AccessControlPolicyTypePermission,
|
|
AccessControlPolicyTypeTeam,
|
|
"mattermost-ai.agent",
|
|
"mattermost-ai:",
|
|
":agent",
|
|
"ab:agent", // plugin ID shorter than MinIdLength
|
|
"mattermost ai:agent", // space is not a valid plugin-ID character
|
|
"mattermost-ai:agent:v2", // separator inside the resource type
|
|
"mattermost-ai:agent type", // space inside the resource type
|
|
"mattermost-ai:" + strings.Repeat("a", MaxPluginResourceTypeLength+1),
|
|
// One over the column width, with both segments individually valid.
|
|
strings.Repeat("p", MaxPolicyTypeLength-MaxPluginResourceTypeLength) + ":" + strings.Repeat("a", MaxPluginResourceTypeLength),
|
|
"",
|
|
}
|
|
for _, policyType := range invalid {
|
|
t.Run("invalid "+policyType, func(t *testing.T) {
|
|
_, _, ok := SplitPluginAccessControlPolicyType(policyType)
|
|
require.False(t, ok)
|
|
require.False(t, IsPluginAccessControlPolicyType(policyType))
|
|
require.False(t, PluginOwnsAccessControlPolicyType("mattermost-ai", policyType))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPluginOwnsAccessControlPolicyType(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
pluginID string
|
|
policyType string
|
|
want bool
|
|
}{
|
|
{"owner matches", "mattermost-ai", "mattermost-ai:agent", true},
|
|
{"foreign plugin", "other-plugin", "mattermost-ai:agent", false},
|
|
{"empty plugin ID", "", "mattermost-ai:agent", false},
|
|
{"core policy type", "mattermost-ai", AccessControlPolicyTypeChannel, false},
|
|
// The stored type is matched byte-for-byte on delete and during
|
|
// evaluation, so a case variant that could Get but never Delete would
|
|
// be a trap.
|
|
{"caller ID case mismatch", "Mattermost-AI", "mattermost-ai:agent", false},
|
|
{"stored prefix case mismatch", "mattermost-ai", "Mattermost-AI:agent", false},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
require.Equal(t, tc.want, PluginOwnsAccessControlPolicyType(tc.pluginID, tc.policyType))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsValidPolicyAction(t *testing.T) {
|
|
for _, action := range []string{
|
|
AccessControlPolicyActionMembership,
|
|
AccessControlPolicyActionUploadFileAttachment,
|
|
"use",
|
|
"invoke_tool",
|
|
"read2",
|
|
} {
|
|
require.True(t, IsValidPolicyAction(action), action)
|
|
}
|
|
|
|
for _, action := range []string{
|
|
"",
|
|
"*",
|
|
"Use",
|
|
"use it",
|
|
"use-it",
|
|
"_use",
|
|
"use_",
|
|
"use__it",
|
|
strings.Repeat("a", MaxPolicyActionLength+1),
|
|
} {
|
|
require.False(t, IsValidPolicyAction(action), action)
|
|
}
|
|
}
|
|
|
|
func TestInheritV0_5Rejected(t *testing.T) {
|
|
parent := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: AccessControlPolicyTypeParent,
|
|
Name: "Parent",
|
|
Version: AccessControlPolicyVersionV0_3,
|
|
Revision: 0,
|
|
Rules: []AccessControlPolicyRule{{
|
|
Actions: []string{AccessControlPolicyActionMembership},
|
|
Expression: "true",
|
|
}},
|
|
}
|
|
child := &AccessControlPolicy{
|
|
ID: NewId(),
|
|
Type: testPluginPolicyType,
|
|
Name: "Agent policy",
|
|
Version: AccessControlPolicyVersionV0_5,
|
|
Revision: 0,
|
|
Rules: []AccessControlPolicyRule{{
|
|
Actions: []string{"use"},
|
|
Expression: "true",
|
|
}},
|
|
}
|
|
err := child.Inherit(parent)
|
|
require.NotNil(t, err)
|
|
require.Equal(t, "model.access_policy.inherit.version.app_error", err.Id)
|
|
require.Empty(t, child.Imports)
|
|
}
|