agent-exec transport f7332a745364f439ad5fdc66fc3e3ec5/3040807e4420
This commit is contained in:
@@ -223,7 +223,11 @@ func capabilityRolePermissions(t *testing.T, st *Store, roleID string) map[strin
|
||||
|
||||
var linkedManagementActions = []string{"project.settings", "project.members.manage", "project.permissions.manage", "document.duties.assign"}
|
||||
|
||||
func TestCapabilityAssociationProjectCreateOnlyLinksFullManagement(t *testing.T) {
|
||||
// TestRoleProjectCreateLinksFullManagement keeps the projection contract that
|
||||
// #628 consumers still read: a role whose single project-access boolean is on
|
||||
// links the own-project management actions, while the role itself stores that
|
||||
// one boolean and nothing else.
|
||||
func TestRoleProjectCreateLinksFullManagement(t *testing.T) {
|
||||
st := openTestStore(t)
|
||||
_, alice, _ := rolePolicyAccounts(t, st)
|
||||
role, err := st.CreateRole("admin", RoleCreate{Name: "项目负责人"})
|
||||
@@ -235,16 +239,14 @@ func TestCapabilityAssociationProjectCreateOnlyLinksFullManagement(t *testing.T)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Join(receipt.LinkedExpansion, ",") != strings.Join(linkedManagementActions, ",") {
|
||||
// The narrowed model expands nothing into role rows.
|
||||
if len(receipt.LinkedExpansion) != 0 {
|
||||
t.Fatalf("expansion=%v", receipt.LinkedExpansion)
|
||||
}
|
||||
permissions := capabilityRolePermissions(t, st, role.RoleID)
|
||||
for _, actionID := range linkedManagementActions {
|
||||
permission, ok := permissions[actionID]
|
||||
// The cap opens the creator's own projects; the default stays off.
|
||||
if !ok || permission.Allowed || !permission.CapAllowed {
|
||||
t.Fatalf("%s=%+v ok=%v", actionID, permission, ok)
|
||||
}
|
||||
access := permissions["project.create"]
|
||||
if len(permissions) != 1 || !access.Allowed || !access.CapAllowed {
|
||||
t.Fatalf("permissions=%+v", permissions)
|
||||
}
|
||||
if _, err := st.AssignUserRole("admin", alice.ID, role.RoleID, alice.StateVersion, "assign-creator"); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -257,12 +259,15 @@ func TestCapabilityAssociationProjectCreateOnlyLinksFullManagement(t *testing.T)
|
||||
t.Fatalf("linked=%+v", linked)
|
||||
}
|
||||
if replayed, err := st.ReplaceRolePermissions("admin", role.RoleID, role.RoleVersion,
|
||||
[]RolePermissionChange{{ActionID: "project.create", Allowed: true, CapAllowed: true}}, "create-only"); err != nil || replayed.ReceiptID != receipt.ReceiptID || len(replayed.LinkedExpansion) != 4 {
|
||||
[]RolePermissionChange{{ActionID: "project.create", Allowed: true, CapAllowed: true}}, "create-only"); err != nil || replayed.ReceiptID != receipt.ReceiptID || len(replayed.LinkedExpansion) != 0 {
|
||||
t.Fatalf("replay=%+v err=%v", replayed, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCapabilityAssociationRejectsExplicitConflictAtomically(t *testing.T) {
|
||||
// TestRolePolicyRejectsNonProjectAccessRowsAtomically covers the narrowed
|
||||
// model's fail-closed writer: anything other than the single project.create
|
||||
// boolean is refused whole, never partially applied.
|
||||
func TestRolePolicyRejectsNonProjectAccessRowsAtomically(t *testing.T) {
|
||||
st := openTestStore(t)
|
||||
rolePolicyAccounts(t, st)
|
||||
role, err := st.CreateRole("admin", RoleCreate{Name: "受限创建者"})
|
||||
@@ -282,24 +287,43 @@ func TestCapabilityAssociationRejectsExplicitConflictAtomically(t *testing.T) {
|
||||
if len(capabilityRolePermissions(t, st, role.RoleID)) != 0 || after.RoleVersion != role.RoleVersion || afterEpoch != epoch {
|
||||
t.Fatalf("partial write: role=%+v epoch=%d->%d", after, epoch, afterEpoch)
|
||||
}
|
||||
// A cap already closed on the role still blocks a later project.create.
|
||||
capped := []RolePermissionChange{{ActionID: "project.members.manage", Allowed: false, CapAllowed: false}}
|
||||
receipt, err := st.ReplaceRolePermissions("admin", role.RoleID, role.RoleVersion, capped, "platform-cap")
|
||||
// A linked action on its own is the same rejection: the role has no row to
|
||||
// cap off any more, so nothing is written.
|
||||
for i, only := range [][]RolePermissionChange{
|
||||
{{ActionID: "project.members.manage", Allowed: false, CapAllowed: false}},
|
||||
{{ActionID: "project.settings", Allowed: true, CapAllowed: true}},
|
||||
{{ActionID: "document.duties.assign", Allowed: false, CapAllowed: true}},
|
||||
} {
|
||||
if _, err := st.ReplaceRolePermissions("admin", role.RoleID, role.RoleVersion, only, "linked-only-"+string(rune('a'+i))); !errors.Is(err, ErrInvalidRolePolicy) {
|
||||
t.Fatalf("linked-only batch %d accepted: %v", i, err)
|
||||
}
|
||||
if permissions := capabilityRolePermissions(t, st, role.RoleID); len(permissions) != 0 {
|
||||
t.Fatalf("linked-only batch %d persisted=%+v", i, permissions)
|
||||
}
|
||||
}
|
||||
receipt, err := st.ReplaceRolePermissions("admin", role.RoleID, role.RoleVersion,
|
||||
[]RolePermissionChange{{ActionID: "project.create", Allowed: true, CapAllowed: true}}, "enable-create")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
enable := []RolePermissionChange{{ActionID: "project.create", Allowed: true, CapAllowed: true}}
|
||||
if _, err := st.ReplaceRolePermissions("admin", role.RoleID, receipt.RoleVersion, enable, "enable-over-cap"); !errors.Is(err, ErrInvalidRolePolicy) {
|
||||
t.Fatalf("existing platform cap conflict: %v", err)
|
||||
if len(receipt.LinkedExpansion) != 0 {
|
||||
t.Fatalf("receipt=%+v", receipt)
|
||||
}
|
||||
permissions := capabilityRolePermissions(t, st, role.RoleID)
|
||||
if _, ok := permissions["project.create"]; ok || len(permissions) != 1 || permissions["project.members.manage"].CapAllowed {
|
||||
t.Fatalf("permissions after rejected enable=%+v", permissions)
|
||||
access := permissions["project.create"]
|
||||
if len(permissions) != 1 || !access.Allowed || !access.CapAllowed {
|
||||
t.Fatalf("permissions=%+v", permissions)
|
||||
}
|
||||
resolved := append(enable, RolePermissionChange{ActionID: "project.members.manage", Allowed: false, CapAllowed: true})
|
||||
if _, err := st.ReplaceRolePermissions("admin", role.RoleID, receipt.RoleVersion, resolved, "explicitly-resolved"); err != nil {
|
||||
// A later batch cannot resolve a conflict either, because no second row is
|
||||
// accepted beside the project-access boolean.
|
||||
resolved := append([]RolePermissionChange{{ActionID: "project.create", Allowed: true, CapAllowed: true}},
|
||||
RolePermissionChange{ActionID: "project.members.manage", Allowed: false, CapAllowed: true})
|
||||
if _, err := st.ReplaceRolePermissions("admin", role.RoleID, receipt.RoleVersion, resolved, "explicitly-resolved"); !errors.Is(err, ErrInvalidRolePolicy) {
|
||||
t.Fatalf("explicit resolution: %v", err)
|
||||
}
|
||||
if permissions := capabilityRolePermissions(t, st, role.RoleID); len(permissions) != 1 || !permissions["project.create"].Allowed {
|
||||
t.Fatalf("rejected resolution changed permissions=%+v", permissions)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCapabilityAssociationRejectsStaleVersionAndMissingRole(t *testing.T) {
|
||||
@@ -323,9 +347,12 @@ func TestCapabilityAssociationRejectsStaleVersionAndMissingRole(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestLinkedCapabilitiesSystemGrantWithoutRoleClosesLoop keeps the fail-closed
|
||||
// projection that #628 consumers still read: a direct project.create system
|
||||
// grant closes the loop on its own, and a role can no longer take it away.
|
||||
func TestLinkedCapabilitiesSystemGrantWithoutRoleClosesLoop(t *testing.T) {
|
||||
st := openTestStore(t)
|
||||
_, alice, bob := rolePolicyAccounts(t, st)
|
||||
_, _, bob := rolePolicyAccounts(t, st)
|
||||
if linked, err := st.LinkedCapabilities("bob"); err != nil || linked.ProjectCreate || len(linked.LinkedActions) != 0 {
|
||||
t.Fatalf("ungranted linked=%+v err=%v", linked, err)
|
||||
}
|
||||
@@ -344,30 +371,8 @@ func TestLinkedCapabilitiesSystemGrantWithoutRoleClosesLoop(t *testing.T) {
|
||||
t.Fatalf("admin linked=%+v err=%v", admin, err)
|
||||
}
|
||||
|
||||
capped, err := st.CreateRole("admin", RoleCreate{Name: "禁止管理"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cappedReceipt, err := st.ReplaceRolePermissions("admin", capped.RoleID, capped.RoleVersion,
|
||||
[]RolePermissionChange{{ActionID: "document.duties.assign", Allowed: false, CapAllowed: false}}, "capped-role")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bob, _ = st.GetAccount(bob.ID)
|
||||
if _, err := st.AssignUserRole("admin", bob.ID, capped.RoleID, bob.StateVersion, "assign-capped-to-creator"); !errors.Is(err, ErrInvalidRolePolicy) {
|
||||
t.Fatalf("capped role bound to project.create grant holder: %v", err)
|
||||
}
|
||||
if _, err := st.AssignUserRole("admin", alice.ID, capped.RoleID, alice.StateVersion, "assign-capped"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
epoch, _ = st.AuthorityEpoch()
|
||||
if _, err := st.ReplaceSystemGrants("admin", alice.ID, epoch, []string{"project.create"}, "grant-capped-create"); !errors.Is(err, ErrInvalidRolePolicy) {
|
||||
t.Fatalf("system grant over role cap: %v", err)
|
||||
}
|
||||
if grants, _, err := st.SystemGrantsForUser(alice.ID); err != nil || len(grants) != 0 {
|
||||
t.Fatalf("rejected grant persisted=%v err=%v", grants, err)
|
||||
}
|
||||
// A role writer cannot later close a linked cap under a grant holder either.
|
||||
// A role carries one project-access boolean, so binding a roleless one
|
||||
// neither closes nor reopens the direct grant.
|
||||
open, err := st.CreateRole("admin", RoleCreate{Name: "普通职位"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -376,6 +381,14 @@ func TestLinkedCapabilitiesSystemGrantWithoutRoleClosesLoop(t *testing.T) {
|
||||
if _, err := st.AssignUserRole("admin", bob.ID, open.RoleID, bob.StateVersion, "assign-open"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
linked, err = st.LinkedCapabilities("bob")
|
||||
if err != nil || linked.Source != "system-grant" || strings.Join(linked.LinkedActions, ",") != strings.Join(linkedManagementActions, ",") {
|
||||
t.Fatalf("grant holder with roleless role linked=%+v err=%v", linked, err)
|
||||
}
|
||||
if permissions := capabilityRolePermissions(t, st, open.RoleID); len(permissions) != 0 {
|
||||
t.Fatalf("roleless role kept permissions=%+v", permissions)
|
||||
}
|
||||
// A linked row cannot be written beside that grant holder either.
|
||||
if _, err := st.ReplaceRolePermissions("admin", open.RoleID, open.RoleVersion,
|
||||
[]RolePermissionChange{{ActionID: "project.settings", Allowed: false, CapAllowed: false}}, "cap-under-holder"); !errors.Is(err, ErrInvalidRolePolicy) {
|
||||
t.Fatalf("role cap under grant holder: %v", err)
|
||||
@@ -383,49 +396,57 @@ func TestLinkedCapabilitiesSystemGrantWithoutRoleClosesLoop(t *testing.T) {
|
||||
if permissions := capabilityRolePermissions(t, st, open.RoleID); len(permissions) != 0 {
|
||||
t.Fatalf("rejected role cap persisted: %+v", permissions)
|
||||
}
|
||||
if role, _ := st.GetRole(capped.RoleID); role.RoleVersion != cappedReceipt.RoleVersion {
|
||||
t.Fatalf("capped role changed: %+v", role)
|
||||
if role, _ := st.GetRole(open.RoleID); role.RoleVersion != open.RoleVersion {
|
||||
t.Fatalf("rejected write bumped role version: %+v", role)
|
||||
}
|
||||
|
||||
// Revoking the grant closes the loop again and reports it.
|
||||
epoch, _ = st.AuthorityEpoch()
|
||||
revoked, err := st.ReplaceSystemGrants("admin", bob.ID, epoch, nil, "revoke-bob-create")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(revoked.MigrationNote, "project.create disabled") {
|
||||
t.Fatalf("revoke receipt=%+v", revoked)
|
||||
}
|
||||
if linked, err := st.LinkedCapabilities("bob"); err != nil || linked.ProjectCreate || len(linked.LinkedActions) != 0 {
|
||||
t.Fatalf("revoked linked=%+v err=%v", linked, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCapabilityAssociationDisablingProjectCreateKeepsLinkedRows(t *testing.T) {
|
||||
// TestRolePolicyDisablingProjectCreateClearsSingleBoolean covers the narrowed
|
||||
// model end to end: the role holds one project-access row, turning it off stops
|
||||
// the projection, and removing it is an explicit second write.
|
||||
func TestRolePolicyDisablingProjectCreateClearsSingleBoolean(t *testing.T) {
|
||||
st := openTestStore(t)
|
||||
_, alice, bob := rolePolicyAccounts(t, st)
|
||||
role, err := st.CreateRole("admin", RoleCreate{Name: "创建者"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
receipt, err := st.ReplaceRolePermissions("admin", role.RoleID, role.RoleVersion, []RolePermissionChange{
|
||||
{ActionID: "project.create", Allowed: true, CapAllowed: true},
|
||||
{ActionID: "project.settings", Allowed: true, CapAllowed: true},
|
||||
}, "enable-create")
|
||||
receipt, err := st.ReplaceRolePermissions("admin", role.RoleID, role.RoleVersion,
|
||||
[]RolePermissionChange{{ActionID: "project.create", Allowed: true, CapAllowed: true}}, "enable-create")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := st.AssignUserRole("admin", alice.ID, role.RoleID, alice.StateVersion, "assign-alice"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if allowed, capAllowed, err := st.RolePermissionAllowed("alice", "project.create"); err != nil || !allowed || !capAllowed {
|
||||
t.Fatalf("enabled role access=%v cap=%v err=%v", allowed, capAllowed, err)
|
||||
}
|
||||
// Disabling is one more project.create row; there are no linked rows left.
|
||||
disabled, err := st.ReplaceRolePermissions("admin", role.RoleID, receipt.RoleVersion,
|
||||
[]RolePermissionChange{{ActionID: "document.read", Allowed: true, CapAllowed: true}}, "disable-create")
|
||||
[]RolePermissionChange{{ActionID: "project.create", Allowed: false, CapAllowed: true}}, "disable-create")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
permissions := capabilityRolePermissions(t, st, role.RoleID)
|
||||
if _, ok := permissions["project.create"]; ok {
|
||||
t.Fatalf("project.create still present: %+v", permissions)
|
||||
access := permissions["project.create"]
|
||||
if len(permissions) != 1 || access.Allowed || access.CapAllowed {
|
||||
t.Fatalf("permissions after disable=%+v", permissions)
|
||||
}
|
||||
if settings := permissions["project.settings"]; !settings.Allowed || !settings.CapAllowed {
|
||||
t.Fatalf("project.settings not kept: %+v", permissions)
|
||||
}
|
||||
for _, actionID := range linkedManagementActions {
|
||||
if _, ok := permissions[actionID]; !ok {
|
||||
t.Fatalf("%s deleted by disabling project.create: %+v", actionID, permissions)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(disabled.MigrationNote, "project.create disabled") || !strings.Contains(disabled.MigrationNote, "project.settings") {
|
||||
t.Fatalf("migration note=%q", disabled.MigrationNote)
|
||||
}
|
||||
if linked, err := st.LinkedCapabilities("alice"); err != nil || linked.ProjectCreate {
|
||||
if linked, err := st.LinkedCapabilities("alice"); err != nil || linked.ProjectCreate || linked.Source != "" {
|
||||
t.Fatalf("disabled create still linked=%+v err=%v", linked, err)
|
||||
}
|
||||
// Removal after project.create is closed is an explicit second write.
|
||||
|
||||
Reference in New Issue
Block a user