Files
sub2api/backend/internal/service/user_can_bind_group_test.go
T
akihitohyhandClaude Opus 5 b56c61eccd feat(admin): let admins restrict which public groups a user may access
Public groups have always been bindable by every user: CanBindGroup returned
true for any non-exclusive group, and user_allowed_groups only ever carried the
exclusive groups an admin had granted. Admins had no way to hand a single user
a subset of the public groups short of converting a group to exclusive, which
changes it for everyone already using it.

A user now carries restrict_public_groups. While it is false, which is the
default and what every existing row migrates to, nothing changes: every public
group stays bindable. Once an admin turns it on for a user, that user's public
groups are narrowed to the ones listed in user_allowed_groups, the same table
that already gates exclusive groups.

The flag is an administrative control, so it rides on the admin user DTO only
and leaves the shape of the end-user endpoints alone.

The model plaza filter honours the flag as well, so a restricted user is not
shown groups they would be refused when binding a key. Anonymous visitors have
no user record and keep the previous view.

Enforcement rides on the existing CanBindGroup choke point, so it covers key
creation, key updates, and per-request authorization together. An API key bound
to a group that is later withdrawn stops working at request time rather than
lingering as a key that can be listed but not used.

The admin dialog gains a toggle over the public group list. Turning it off
re-checks every public group, so an admin cannot save a list that reads as
restrictive while the restriction itself is disabled.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 02:40:17 +08:00

88 lines
2.3 KiB
Go

package service
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestUserCanBindGroup(t *testing.T) {
const (
publicGroupA int64 = 10
publicGroupB int64 = 11
exclusiveGroupA int64 = 20
)
for _, tc := range []struct {
name string
user User
groupID int64
isExclusive bool
want bool
}{
{
name: "public group is bindable by default",
user: User{},
groupID: publicGroupA,
want: true,
},
{
name: "exclusive group needs an explicit grant",
user: User{},
groupID: exclusiveGroupA,
isExclusive: true,
want: false,
},
{
name: "granted exclusive group is bindable",
user: User{AllowedGroups: []int64{exclusiveGroupA}},
groupID: exclusiveGroupA,
isExclusive: true,
want: true,
},
{
name: "an unrestricted user keeps every public group even with a grant list",
user: User{AllowedGroups: []int64{exclusiveGroupA}},
groupID: publicGroupA,
want: true,
},
{
name: "a restricted user keeps the public groups listed for them",
user: User{RestrictPublicGroups: true, AllowedGroups: []int64{publicGroupA}},
groupID: publicGroupA,
want: true,
},
{
name: "a restricted user loses the public groups not listed for them",
user: User{RestrictPublicGroups: true, AllowedGroups: []int64{publicGroupA}},
groupID: publicGroupB,
want: false,
},
{
name: "a restricted user with no list keeps no public group",
user: User{RestrictPublicGroups: true},
groupID: publicGroupA,
want: false,
},
{
name: "restricting public groups does not widen exclusive access",
user: User{RestrictPublicGroups: true, AllowedGroups: []int64{publicGroupA}},
groupID: exclusiveGroupA,
isExclusive: true,
want: false,
},
{
name: "a restricted user can still hold both kinds of grant",
user: User{RestrictPublicGroups: true, AllowedGroups: []int64{publicGroupA, exclusiveGroupA}},
groupID: exclusiveGroupA,
isExclusive: true,
want: true,
},
} {
t.Run(tc.name, func(t *testing.T) {
user := tc.user
require.Equal(t, tc.want, user.CanBindGroup(tc.groupID, tc.isExclusive))
})
}
}