[MM-63595] Add model structs for Access Control Policies (#30589)

Этот коммит содержится в:
Ibrahim Serdar Acikgoz
2025-03-28 14:19:53 +01:00
коммит произвёл GitHub
родитель 2108216818
Коммит c44c139c9e
4 изменённых файлов: 352 добавлений и 1 удалений

Просмотреть файл

@@ -8468,6 +8468,38 @@
"id": "model.access.is_valid.user_id.app_error",
"translation": "Invalid user id."
},
{
"id": "model.access_policy.is_valid.id.app_error",
"translation": "Invalid policyy id."
},
{
"id": "model.access_policy.is_valid.imports.app_error",
"translation": "Invalid imports for the policy."
},
{
"id": "model.access_policy.is_valid.name.app_error",
"translation": "Invalid name for the policy."
},
{
"id": "model.access_policy.is_valid.revision.app_error",
"translation": "Invalid policy revision."
},
{
"id": "model.access_policy.is_valid.rules.app_error",
"translation": "Rule(s) is not valid."
},
{
"id": "model.access_policy.is_valid.rules_imports.app_error",
"translation": "Policy must either import or define rules."
},
{
"id": "model.access_policy.is_valid.type.app_error",
"translation": "Invalid policy type."
},
{
"id": "model.access_policy.is_valid.version.app_error",
"translation": "Version is not valid for this access control policy."
},
{
"id": "model.acknowledgement.is_valid.post_id.app_error",
"translation": "Invalid post id."

Просмотреть файл

@@ -29,6 +29,7 @@ require (
github.com/tinylib/msgp v1.2.5
github.com/vmihailenco/msgpack/v5 v5.4.1
golang.org/x/crypto v0.32.0
golang.org/x/mod v0.22.0
golang.org/x/net v0.34.0
golang.org/x/oauth2 v0.25.0
golang.org/x/text v0.21.0
@@ -66,7 +67,6 @@ require (
github.com/wiggin77/merror v1.0.5 // indirect
github.com/wiggin77/srslog v1.0.1 // indirect
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
golang.org/x/mod v0.22.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.29.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250124145028-65684f501c47 // indirect

105
server/public/model/access_policy.go Обычный файл
Просмотреть файл

@@ -0,0 +1,105 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"slices"
"golang.org/x/mod/semver"
)
const (
AccessControlPolicyTypeParent = "parent"
AccessControlPolicyTypeChannel = "channel"
MaxPolicyNameLength = 128
AccessControlPolicyVersionV0_1 = "v0.1"
)
// ParentPolicy is a augmented version of AccessPolicy to be used in
// system console and API responses.
type ParentPolicy struct {
ID string `json:"id"`
Name string `json:"name"`
Attributes map[string]string `json:"attributes"`
Children []*AccessControlPolicy `json:"children"`
}
type AccessControlPolicy struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Active bool `json:"active"`
CreateAt int64 `json:"create_at"`
Revision int `json:"revision"`
Version string `json:"version"`
Imports []string `json:"imports"`
Rules []AccessControlPolicyRule `json:"rules"`
Props map[string]any `json:"props"` // add auto-sync property here, also maybe the attributes being used in the expression
}
type AccessControlPolicyRule struct {
Actions []string `json:"actions"`
Expression string `json:"expression"`
}
func (p *AccessControlPolicy) IsValid() *AppError {
switch p.Version {
case AccessControlPolicyVersionV0_1:
return p.accessPolicyVersionV0_1()
default:
return NewAppError("AccessControlPolicy.IsValid", "model.access_policy.is_valid.version.app_error", nil, "", 400)
}
}
func (p *AccessControlPolicy) accessPolicyVersionV0_1() *AppError {
if !slices.Contains([]string{AccessControlPolicyTypeParent, AccessControlPolicyTypeChannel}, p.Type) {
return NewAppError("AccessControlPolicy.IsValid", "model.access_policy.is_valid.type.app_error", nil, "", 400)
}
if !IsValidId(p.ID) {
return NewAppError("AccessControlPolicy.IsValid", "model.access_policy.is_valid.id.app_error", nil, "", 400)
}
if p.Type == AccessControlPolicyTypeParent && (p.Name == "" || len(p.Name) > MaxPolicyNameLength) {
return NewAppError("AccessControlPolicy.IsValid", "model.access_policy.is_valid.name.app_error", nil, "", 400)
}
if p.Revision < 0 {
return NewAppError("AccessControlPolicy.IsValid", "model.access_policy.is_valid.revision.app_error", nil, "", 400)
}
if !semver.IsValid(p.Version) {
return NewAppError("AccessControlPolicy.IsValid", "model.access_policy.is_valid.version.app_error", nil, "", 400)
}
switch p.Type {
case AccessControlPolicyTypeParent:
if len(p.Rules) == 0 {
return NewAppError("AccessControlPolicy.IsValid", "model.access_policy.is_valid.rules.app_error", nil, "", 400)
}
if len(p.Imports) > 0 {
return NewAppError("AccessControlPolicy.IsValid", "model.access_policy.is_valid.imports.app_error", nil, "", 400)
}
case AccessControlPolicyTypeChannel:
if len(p.Rules) == 0 && len(p.Imports) == 0 {
return NewAppError("AccessControlPolicy.IsValid", "model.access_policy.is_valid.rules_imports.app_error", nil, "", 400)
}
if len(p.Rules) == 0 {
return NewAppError("AccessControlPolicy.IsValid", "model.access_policy.is_valid.rules.app_error", nil, "", 400)
}
if len(p.Imports) > 1 {
return NewAppError("AccessControlPolicy.IsValid", "model.access_policy.is_valid.imports.app_error", nil, "", 400)
}
}
return nil
}

214
server/public/model/access_policy_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,214 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"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) {
longName := ""
for i := 0; i <= MaxPolicyNameLength; i++ {
longName += "a"
}
policy := &AccessControlPolicy{
ID: NewId(),
Type: AccessControlPolicyTypeParent,
Name: longName,
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")
})
}