From 2e388d4cb8f9d8bc35ebdc3bcb57e7f4a21abd5e Mon Sep 17 00:00:00 2001 From: awatercolorpen Date: Wed, 22 Apr 2026 13:03:05 +0800 Subject: [PATCH 1/5] docs: add usage examples for multi-domain, role inheritance, permission checks, and frontend/backend separation --- docs/examples.md | 494 ++++++++++++++++++ .../specs/2026-03-10-caskin-modernization.md | 2 +- 2 files changed, 495 insertions(+), 1 deletion(-) create mode 100644 docs/examples.md diff --git a/docs/examples.md b/docs/examples.md new file mode 100644 index 0000000..90c7c68 --- /dev/null +++ b/docs/examples.md @@ -0,0 +1,494 @@ +# Usage Examples + +This document covers common real-world scenarios for caskin. Each example is +self-contained and uses the types defined in the [`example/`](../example/) +package. All examples assume you have already set up a service via +[Getting Started](./getting-started.md). + +--- + +## Table of Contents + +- [Setup Helper](#setup-helper) +- [Scenario 1: Multi-Domain Management](#scenario-1-multi-domain-management) +- [Scenario 2: Role Inheritance](#scenario-2-role-inheritance) +- [Scenario 3: Permission Checks](#scenario-3-permission-checks) +- [Scenario 4: Frontend / Backend Permission Separation](#scenario-4-frontend--backend-permission-separation) + +--- + +## Setup Helper + +All examples below share this setup function that creates a working caskin +service backed by an in-memory SQLite database. + +```go +package main + +import ( + "os" + + "github.com/awatercolorpen/caskin" + "github.com/awatercolorpen/caskin/example" + "gorm.io/gorm" +) + +// newService creates an in-memory caskin service for demonstration. +func newService() (caskin.IService, *gorm.DB) { + dir, _ := os.MkdirTemp("", "caskin-example-*") + dbOption := &caskin.DBOption{ + DSN: dir + "/sqlite", + Type: "sqlite", + } + db, _ := dbOption.NewDB() + _ = db.AutoMigrate( + &example.User{}, + &example.Role{}, + &example.Object{}, + &example.Domain{}, + ) + + dictOption := &caskin.DictionaryOption{ + ModelText: caskin.DefaultModelText(), + } + dict, _ := dictOption.Build() + + svc, _ := caskin.New( + caskin.WithDB(db), + caskin.WithDictionary(dict), + ) + return svc, db +} +``` + +--- + +## Scenario 1: Multi-Domain Management + +caskin is designed for **multi-tenant** systems. Each domain is an isolated +permission scope — users, roles, and policies in one domain do not affect +another. + +### Create and bootstrap multiple domains + +```go +func multiDomainExample() { + svc, _ := newService() + + // --- Create two tenants (domains) --- + engineering := &example.Domain{Name: "engineering"} + marketing := &example.Domain{Name: "marketing"} + _ = svc.CreateDomain(engineering) + _ = svc.CreateDomain(marketing) + + // Bootstrap each domain: creates the built-in admin/member roles and + // the root object tree defined by the dictionary. + _ = svc.ResetDomain(engineering) + _ = svc.ResetDomain(marketing) + _ = svc.ResetFeature(engineering) + _ = svc.ResetFeature(marketing) + + // --- Create a shared superadmin --- + superadmin := &example.User{Email: "admin@company.com"} + _ = svc.CreateUser(superadmin) + _ = svc.AddSuperadmin(superadmin) + + // --- Create domain-specific admins --- + engAdmin := &example.User{Email: "eng-lead@company.com"} + mktAdmin := &example.User{Email: "mkt-lead@company.com"} + for _, u := range []caskin.User{engAdmin, mktAdmin} { + _ = svc.CreateUser(u) + } + + // Assign engAdmin as admin of the engineering domain only. + engRoles, _ := svc.GetRole(superadmin, engineering) + // engRoles[0] is the admin role created by ResetDomain. + _ = svc.ModifyUserRolePerRole(superadmin, engineering, engRoles[0], + []*caskin.UserRolePair{{User: engAdmin, Role: engRoles[0]}}, + ) + + // Assign mktAdmin as admin of the marketing domain only. + mktRoles, _ := svc.GetRole(superadmin, marketing) + _ = svc.ModifyUserRolePerRole(superadmin, marketing, mktRoles[0], + []*caskin.UserRolePair{{User: mktAdmin, Role: mktRoles[0]}}, + ) + + // --- Verify isolation --- + // engAdmin cannot see any roles in the marketing domain. + rolesSeenByEngAdmin, _ := svc.GetRole(engAdmin, marketing) + fmt.Println("eng admin sees marketing roles:", len(rolesSeenByEngAdmin)) // 0 + + // The superadmin can see all domains. + domains, _ := svc.GetDomain(superadmin) + fmt.Println("total domains:", len(domains)) // 2 +} +``` + +### Key points + +| Behaviour | Detail | +|---|---| +| Domain isolation | Roles, objects, and policies are scoped per domain | +| Superadmin bypass | Superadmins can act across all domains | +| `ResetDomain` | Must be called after `CreateDomain` to initialise the built-in role/object tree | + +--- + +## Scenario 2: Role Inheritance + +caskin supports **role inheritance** (also called role hierarchies). A child +role automatically inherits all permissions of its parent role. + +```go +func roleInheritanceExample() { + svc, _ := newService() + + domain := &example.Domain{Name: "app"} + _ = svc.CreateDomain(domain) + _ = svc.ResetDomain(domain) + _ = svc.ResetFeature(domain) + + superadmin := &example.User{Email: "root@example.com"} + _ = svc.CreateUser(superadmin) + _ = svc.AddSuperadmin(superadmin) + + // --- Build a three-level role hierarchy --- + // viewer ← editor ← owner + // (viewer has fewest, owner has most permissions) + + viewer := &example.Role{Name: "viewer", DomainID: domain.GetID()} + editor := &example.Role{Name: "editor", DomainID: domain.GetID()} + owner := &example.Role{Name: "owner", DomainID: domain.GetID()} + + for _, r := range []caskin.ObjectData{viewer, editor, owner} { + _ = svc.CreateObjectData(superadmin, domain, r, caskin.ObjectTypeRole) + } + + // editor inherits from viewer (editor >= viewer) + _ = svc.AddRoleG(superadmin, domain, editor, viewer) + + // owner inherits from editor (owner >= editor >= viewer) + _ = svc.AddRoleG(superadmin, domain, owner, editor) + + // --- Grant baseline permissions to viewer on a resource object --- + objects, _ := svc.GetObject(superadmin, domain, caskin.Read) + // Use the first non-root object as the demo resource. + var resource caskin.Object + for _, o := range objects { + if o.GetParentID() != 0 { + resource = o + break + } + } + + // viewer: read-only + _ = svc.ModifyPolicyPerRole(superadmin, domain, viewer, + []*caskin.Policy{{Role: viewer, Object: resource, Domain: domain, Action: caskin.Read}}, + ) + // editor: also write + _ = svc.ModifyPolicyPerRole(superadmin, domain, editor, + []*caskin.Policy{{Role: editor, Object: resource, Domain: domain, Action: caskin.Write}}, + ) + // owner: also manage + _ = svc.ModifyPolicyPerRole(superadmin, domain, owner, + []*caskin.Policy{{Role: owner, Object: resource, Domain: domain, Action: caskin.Manage}}, + ) + + // --- Assign users to roles --- + alice := &example.User{Email: "alice@example.com"} + bob := &example.User{Email: "bob@example.com"} + carol := &example.User{Email: "carol@example.com"} + for _, u := range []caskin.User{alice, bob, carol} { + _ = svc.CreateUser(u) + } + + _ = svc.ModifyUserRolePerRole(superadmin, domain, viewer, + []*caskin.UserRolePair{{User: alice, Role: viewer}}) + _ = svc.ModifyUserRolePerRole(superadmin, domain, editor, + []*caskin.UserRolePair{{User: bob, Role: editor}}) + _ = svc.ModifyUserRolePerRole(superadmin, domain, owner, + []*caskin.UserRolePair{{User: carol, Role: owner}}) + + // --- Verify that Bob (editor) inherits viewer permissions --- + // Bob should be able to read (inherited) and write (direct). + canRead := caskin.Check(svc.GetEnforcer(), bob, domain, resource, caskin.Read) + canWrite := caskin.Check(svc.GetEnforcer(), bob, domain, resource, caskin.Write) + fmt.Println("bob can read:", canRead) // true (inherited from viewer) + fmt.Println("bob can write:", canWrite) // true (direct on editor) + + // Alice (viewer) cannot write. + aliceWrite := caskin.Check(svc.GetEnforcer(), alice, domain, resource, caskin.Write) + fmt.Println("alice can write:", aliceWrite) // false + + // --- Remove the editor → viewer link at runtime --- + _ = svc.RemoveRoleG(superadmin, domain, editor, viewer) + // Now Bob no longer inherits viewer's read permission via that path. +} +``` + +### Key points + +| API | Description | +|---|---| +| `AddRoleG(user, domain, from, to)` | `from` inherits all permissions of `to` | +| `RemoveRoleG(user, domain, from, to)` | Remove the inheritance link at runtime | +| Transitivity | Inheritance is transitive: owner → editor → viewer | + +--- + +## Scenario 3: Permission Checks + +There are two ways to check permissions in caskin: + +1. **`caskin.Check`** — low-level, direct enforcer call; returns a `bool`. +2. **`IService` methods** — higher-level; return typed errors and respect the + caller's own permission scope. + +```go +func permissionCheckExample() { + svc, _ := newService() + + domain := &example.Domain{Name: "wiki"} + _ = svc.CreateDomain(domain) + _ = svc.ResetDomain(domain) + _ = svc.ResetFeature(domain) + + superadmin := &example.User{Email: "root@example.com"} + _ = svc.CreateUser(superadmin) + _ = svc.AddSuperadmin(superadmin) + + editor := &example.Role{Name: "editor", DomainID: domain.GetID()} + _ = svc.CreateObjectData(superadmin, domain, editor, caskin.ObjectTypeRole) + + alice := &example.User{Email: "alice@example.com"} + _ = svc.CreateUser(alice) + _ = svc.ModifyUserRolePerRole(superadmin, domain, editor, + []*caskin.UserRolePair{{User: alice, Role: editor}}) + + // Grab a real Object to check against. + objects, _ := svc.GetObject(superadmin, domain, caskin.Read) + var article caskin.Object + for _, o := range objects { + if o.GetParentID() != 0 { + article = o + break + } + } + + // Grant editor the write permission on article. + _ = svc.ModifyPolicyPerRole(superadmin, domain, editor, + []*caskin.Policy{{Role: editor, Object: article, Domain: domain, Action: caskin.Write}}, + ) + + // --- Method 1: low-level boolean check (no user-scope filtering) --- + ok := caskin.Check(svc.GetEnforcer(), alice, domain, article, caskin.Write) + fmt.Println("alice can write (low-level):", ok) // true + + // --- Method 2: service-level check (respects caller's own permissions) --- + err := svc.CheckObject(alice, domain, article, caskin.Write) + fmt.Println("alice write error:", err) // + + err = svc.CheckObject(alice, domain, article, caskin.Manage) + fmt.Println("alice manage error:", err) // "no manage permission" + + // --- Using ICurrentService for middleware-style checks --- + // Bind the current user + domain once (e.g. in an HTTP middleware) and + // then call the Check* methods without passing user/domain on every call. + current := svc.SetCurrent(alice, domain) + err = current.CheckModifyObjectDataWithCurrent(editor) + fmt.Println("alice modify editor (current):", err) // — alice is editor +} +``` + +### Choosing the right check + +| Scenario | Recommended API | +|---|---| +| HTTP middleware / auth gate | `ICurrentService.Check*WithCurrent` after `SetCurrent` | +| Business logic, needs typed error | `IService.CheckObject` / `CheckObjectData` | +| Raw policy inspection / tests | `caskin.Check` (package-level) | + +--- + +## Scenario 4: Frontend / Backend Permission Separation + +A common pattern is to expose **different object trees** to frontend (UI +buttons/pages) and backend (API endpoints). caskin models this naturally +because each `Object` can have a custom type, and you can organise objects into +separate sub-trees. + +```go +// FrontendObject represents a UI element (e.g. a menu item or button). +type FrontendObject struct { + example.Object + // Extra fields meaningful to the frontend, e.g. RouteKey or ComponentName. + RouteKey string `gorm:"column:route_key"` +} + +// BackendObject represents an API endpoint permission. +type BackendObject struct { + example.Object + // Extra fields meaningful to the backend, e.g. HTTP method and path. + Method string `gorm:"column:method"` + Path string `gorm:"column:path"` +} + +func frontendBackendSeparationExample() { + svc, _ := newService() + + domain := &example.Domain{Name: "saas-app"} + _ = svc.CreateDomain(domain) + _ = svc.ResetDomain(domain) + _ = svc.ResetFeature(domain) + + superadmin := &example.User{Email: "root@example.com"} + _ = svc.CreateUser(superadmin) + _ = svc.AddSuperadmin(superadmin) + + // Retrieve the root objects created by ResetFeature. + // By convention, the first root-level object is where you hang your + // custom sub-trees. + rootObjects, _ := svc.GetObject(superadmin, domain, caskin.Read) + var root caskin.Object + for _, o := range rootObjects { + if o.GetParentID() == 0 { + root = o + break + } + } + + // --- Create two top-level "namespace" objects --- + // All frontend objects live under "ui-root". + // All backend objects live under "api-root". + uiRoot := &example.Object{ + Name: "ui-root", + Type: "ui", + ParentID: root.GetID(), + DomainID: domain.GetID(), + } + apiRoot := &example.Object{ + Name: "api-root", + Type: "api", + ParentID: root.GetID(), + DomainID: domain.GetID(), + } + _ = svc.CreateObject(superadmin, domain, uiRoot) + _ = svc.CreateObject(superadmin, domain, apiRoot) + + // --- Add child objects for specific UI pages and API endpoints --- + dashboardPage := &example.Object{ + Name: "/dashboard", Type: "ui", ParentID: uiRoot.GetID(), DomainID: domain.GetID(), + } + settingsPage := &example.Object{ + Name: "/settings", Type: "ui", ParentID: uiRoot.GetID(), DomainID: domain.GetID(), + } + apiUsers := &example.Object{ + Name: "GET /api/users", Type: "api", ParentID: apiRoot.GetID(), DomainID: domain.GetID(), + } + apiUsersWrite := &example.Object{ + Name: "POST /api/users", Type: "api", ParentID: apiRoot.GetID(), DomainID: domain.GetID(), + } + for _, o := range []caskin.Object{dashboardPage, settingsPage, apiUsers, apiUsersWrite} { + _ = svc.CreateObject(superadmin, domain, o) + } + + // --- Create roles with different permission scopes --- + readonly := &example.Role{Name: "readonly", DomainID: domain.GetID()} + fullAccess := &example.Role{Name: "full-access", DomainID: domain.GetID()} + for _, r := range []caskin.ObjectData{readonly, fullAccess} { + _ = svc.CreateObjectData(superadmin, domain, r, caskin.ObjectTypeRole) + } + + // readonly: can see the dashboard and call GET /api/users + _ = svc.ModifyPolicyPerRole(superadmin, domain, readonly, []*caskin.Policy{ + {Role: readonly, Object: dashboardPage, Domain: domain, Action: caskin.Read}, + {Role: readonly, Object: apiUsers, Domain: domain, Action: caskin.Read}, + }) + + // full-access: everything including settings and write APIs + _ = svc.ModifyPolicyPerRole(superadmin, domain, fullAccess, []*caskin.Policy{ + {Role: fullAccess, Object: dashboardPage, Domain: domain, Action: caskin.Read}, + {Role: fullAccess, Object: settingsPage, Domain: domain, Action: caskin.Manage}, + {Role: fullAccess, Object: apiUsers, Domain: domain, Action: caskin.Read}, + {Role: fullAccess, Object: apiUsersWrite, Domain: domain, Action: caskin.Write}, + }) + + // --- Assign users --- + alice := &example.User{Email: "alice@example.com"} // read-only viewer + bob := &example.User{Email: "bob@example.com"} // full-access admin + for _, u := range []caskin.User{alice, bob} { + _ = svc.CreateUser(u) + } + _ = svc.ModifyUserRolePerRole(superadmin, domain, readonly, + []*caskin.UserRolePair{{User: alice, Role: readonly}}) + _ = svc.ModifyUserRolePerRole(superadmin, domain, fullAccess, + []*caskin.UserRolePair{{User: bob, Role: fullAccess}}) + + // --- Simulate what the frontend queries at login --- + // "Which UI pages can Alice see?" + aliceUIObjects := filterByType(mustGetObjects(svc, alice, domain, caskin.Read), "ui") + fmt.Println("alice UI pages:", names(aliceUIObjects)) + // [/dashboard] — /settings is not in her policy + + // --- Simulate what an API gateway checks per request --- + // "Can Alice call POST /api/users?" + canPost := caskin.Check(svc.GetEnforcer(), alice, domain, apiUsersWrite, caskin.Write) + fmt.Println("alice can POST /api/users:", canPost) // false + + canPost = caskin.Check(svc.GetEnforcer(), bob, domain, apiUsersWrite, caskin.Write) + fmt.Println("bob can POST /api/users:", canPost) // true +} + +// --- helpers used above --- + +func mustGetObjects(svc caskin.IService, u caskin.User, d caskin.Domain, a caskin.Action) []caskin.Object { + objs, _ := svc.GetObject(u, d, a) + return objs +} + +func filterByType(objs []caskin.Object, ty caskin.ObjectType) []caskin.Object { + var out []caskin.Object + for _, o := range objs { + if o.GetType() == ty { + out = append(out, o) + } + } + return out +} + +func names(objs []caskin.Object) []string { + out := make([]string, len(objs)) + for i, o := range objs { + out[i] = o.GetName() + } + return out +} +``` + +### Recommended middleware pattern + +```go +// HTTP middleware example (framework-agnostic pseudocode). +func PermissionMiddleware(svc caskin.IService) Middleware { + return func(ctx Context, next Handler) { + user := ctx.CurrentUser().(caskin.User) + domain := ctx.CurrentDomain().(caskin.Domain) + object := lookupObjectForRoute(ctx.Route()).(caskin.Object) + + if !caskin.Check(svc.GetEnforcer(), user, domain, object, caskin.Read) { + ctx.Abort(http.StatusForbidden) + return + } + next(ctx) + } +} +``` + +--- + +## See Also + +- [Getting Started](./getting-started.md) — step-by-step setup from scratch +- [API Reference](./api-reference.md) — full type and method documentation +- [`playground/`](../playground/) — runnable integration test environment +- [`example/`](../example/) — sample implementations of the four core types diff --git a/docs/superpowers/specs/2026-03-10-caskin-modernization.md b/docs/superpowers/specs/2026-03-10-caskin-modernization.md index 18171a5..c403984 100644 --- a/docs/superpowers/specs/2026-03-10-caskin-modernization.md +++ b/docs/superpowers/specs/2026-03-10-caskin-modernization.md @@ -53,7 +53,7 @@ caskin 是一个 Go 的多域 RBAC 权限管理库,基于 casbin 开发。最 - [x] 重写 Getting Started(step-by-step,有完整可运行代码)(PR #29) - [x] API 文档:每个方法说明参数、返回值、使用场景(PR #30,docs/api-reference.md) -- [x] 增加常见使用场景示例(docs/use-cases.md): +- [x] 增加常见使用场景示例(PR #31,docs/use-cases.md): - 多域管理 - 角色继承 - 权限检查 From d9735e6d9ea4c0450e18c9992aec180e003b98bc Mon Sep 17 00:00:00 2001 From: awatercolorpen Date: Wed, 22 Apr 2026 18:09:55 +0800 Subject: [PATCH 2/5] fix(docs/examples): address all four review bugs - Replace non-existent WithDB/WithDictionary/DefaultModelText helpers in newService() with direct caskin.Options{DB, Dictionary} struct - Add required caskin.Register[...U,R,O,D]() call before caskin.New (mirrors playground/playground.go pattern) - Replace svc.GetEnforcer() (IService has no such method) with svc.CheckObject(...) == nil for boolean permission checks - Fix o.GetType() -> o.GetObjectType() in filterByType helper - Update Scenario 3 intro and table to reflect corrected API usage --- docs/examples.md | 48 +++++++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/docs/examples.md b/docs/examples.md index 90c7c68..3789c9f 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -34,6 +34,7 @@ import ( ) // newService creates an in-memory caskin service for demonstration. +// It mirrors the setup in playground/playground.go. func newService() (caskin.IService, *gorm.DB) { dir, _ := os.MkdirTemp("", "caskin-example-*") dbOption := &caskin.DBOption{ @@ -48,15 +49,14 @@ func newService() (caskin.IService, *gorm.DB) { &example.Domain{}, ) - dictOption := &caskin.DictionaryOption{ - ModelText: caskin.DefaultModelText(), - } - dict, _ := dictOption.Build() + // Register must be called before New so the factory knows which + // concrete types to instantiate. + caskin.Register[*example.User, *example.Role, *example.Object, *example.Domain]() - svc, _ := caskin.New( - caskin.WithDB(db), - caskin.WithDictionary(dict), - ) + svc, _ := caskin.New(&caskin.Options{ + DB: dbOption, + Dictionary: &caskin.DictionaryOption{Dsn: "configs/caskin.toml"}, + }) return svc, db } ``` @@ -211,13 +211,14 @@ func roleInheritanceExample() { // --- Verify that Bob (editor) inherits viewer permissions --- // Bob should be able to read (inherited) and write (direct). - canRead := caskin.Check(svc.GetEnforcer(), bob, domain, resource, caskin.Read) - canWrite := caskin.Check(svc.GetEnforcer(), bob, domain, resource, caskin.Write) + // IService exposes CheckObject which returns nil on success. + canRead := svc.CheckObject(bob, domain, resource, caskin.Read) == nil + canWrite := svc.CheckObject(bob, domain, resource, caskin.Write) == nil fmt.Println("bob can read:", canRead) // true (inherited from viewer) fmt.Println("bob can write:", canWrite) // true (direct on editor) // Alice (viewer) cannot write. - aliceWrite := caskin.Check(svc.GetEnforcer(), alice, domain, resource, caskin.Write) + aliceWrite := svc.CheckObject(alice, domain, resource, caskin.Write) == nil fmt.Println("alice can write:", aliceWrite) // false // --- Remove the editor → viewer link at runtime --- @@ -238,11 +239,12 @@ func roleInheritanceExample() { ## Scenario 3: Permission Checks -There are two ways to check permissions in caskin: +caskin exposes two layers for checking permissions: -1. **`caskin.Check`** — low-level, direct enforcer call; returns a `bool`. -2. **`IService` methods** — higher-level; return typed errors and respect the - caller's own permission scope. +1. **`IService.CheckObject`** — service-level check; returns a typed error and + respects the caller's own permission scope. +2. **`ICurrentService.Check*WithCurrent`** — middleware pattern; binds user and + domain once via `SetCurrent` then checks without re-passing them. ```go func permissionCheckExample() { @@ -280,9 +282,9 @@ func permissionCheckExample() { []*caskin.Policy{{Role: editor, Object: article, Domain: domain, Action: caskin.Write}}, ) - // --- Method 1: low-level boolean check (no user-scope filtering) --- - ok := caskin.Check(svc.GetEnforcer(), alice, domain, article, caskin.Write) - fmt.Println("alice can write (low-level):", ok) // true + // --- Method 1: service-level boolean check --- + ok := svc.CheckObject(alice, domain, article, caskin.Write) == nil + fmt.Println("alice can write:", ok) // true // --- Method 2: service-level check (respects caller's own permissions) --- err := svc.CheckObject(alice, domain, article, caskin.Write) @@ -306,7 +308,7 @@ func permissionCheckExample() { |---|---| | HTTP middleware / auth gate | `ICurrentService.Check*WithCurrent` after `SetCurrent` | | Business logic, needs typed error | `IService.CheckObject` / `CheckObjectData` | -| Raw policy inspection / tests | `caskin.Check` (package-level) | +| Testing policy directly (with concrete `*server`) | `caskin.Check(enforcer, ...)` (package-level, not on `IService`) | --- @@ -432,10 +434,10 @@ func frontendBackendSeparationExample() { // --- Simulate what an API gateway checks per request --- // "Can Alice call POST /api/users?" - canPost := caskin.Check(svc.GetEnforcer(), alice, domain, apiUsersWrite, caskin.Write) + canPost := svc.CheckObject(alice, domain, apiUsersWrite, caskin.Write) == nil fmt.Println("alice can POST /api/users:", canPost) // false - canPost = caskin.Check(svc.GetEnforcer(), bob, domain, apiUsersWrite, caskin.Write) + canPost = svc.CheckObject(bob, domain, apiUsersWrite, caskin.Write) == nil fmt.Println("bob can POST /api/users:", canPost) // true } @@ -449,7 +451,7 @@ func mustGetObjects(svc caskin.IService, u caskin.User, d caskin.Domain, a caski func filterByType(objs []caskin.Object, ty caskin.ObjectType) []caskin.Object { var out []caskin.Object for _, o := range objs { - if o.GetType() == ty { + if o.GetObjectType() == ty { out = append(out, o) } } @@ -475,7 +477,7 @@ func PermissionMiddleware(svc caskin.IService) Middleware { domain := ctx.CurrentDomain().(caskin.Domain) object := lookupObjectForRoute(ctx.Route()).(caskin.Object) - if !caskin.Check(svc.GetEnforcer(), user, domain, object, caskin.Read) { + if svc.CheckObject(user, domain, object, caskin.Read) != nil { ctx.Abort(http.StatusForbidden) return } From 15c1d00b8123c18679f728dc971ca93bad0c77cb Mon Sep 17 00:00:00 2001 From: awatercolorpen Date: Wed, 22 Apr 2026 18:12:10 +0800 Subject: [PATCH 3/5] docs: record PR #31 review lessons in modernization spec --- .../specs/2026-03-10-caskin-modernization.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/superpowers/specs/2026-03-10-caskin-modernization.md b/docs/superpowers/specs/2026-03-10-caskin-modernization.md index c403984..fe22265 100644 --- a/docs/superpowers/specs/2026-03-10-caskin-modernization.md +++ b/docs/superpowers/specs/2026-03-10-caskin-modernization.md @@ -58,6 +58,7 @@ caskin 是一个 Go 的多域 RBAC 权限管理库,基于 casbin 开发。最 - 角色继承 - 权限检查 - 前端/后端权限分离 + - ⚠️ Review 修复(2026-04-22):4 个编译阻断 bug 已修复(见下方经验积累) - [x] 架构说明文档(给贡献者看,docs/architecture.md) - [x] CONTRIBUTING.md @@ -74,6 +75,15 @@ caskin 是一个 Go 的多域 RBAC 权限管理库,基于 casbin 开发。最 3. **`caskin.Object` 接口方法名**:是 `GetObjectType()`,不是 `GetType()`,写示例前需对照 `schema.go` 确认接口定义。 4. **`caskin.Register[...]()` 必须在 `New` 前调用**:每个新的示例/测试 setup 函数都要检查是否有这行,否则运行时 panic。 +### Review 经验积累 + +**caskin API 文档示例注意事项(2026-04-22 from PR #31):** + +1. **必须调用 `caskin.Register[U,R,O,D]()`** — 在 `caskin.New` 之前调用,否则 factory 不知道具体类型,运行时 panic +2. **`caskin.New` 接受 `*Options` 结构体** — 无 `WithDB` / `WithDictionary` / `DefaultModelText()` 等 functional options;正确写法:`caskin.New(&caskin.Options{DB: dbOption, Dictionary: &caskin.DictionaryOption{Dsn: "configs/caskin.toml"}})` +3. **`IService` 不暴露 `GetEnforcer()`** — 使用 `svc.CheckObject(user, domain, obj, action) == nil` 替代 `caskin.Check(svc.GetEnforcer(), ...)`;`caskin.Check` 需要具体的 enforcer,不能通过 `IService` 调用 +4. **`caskin.Object` 接口方法是 `GetObjectType()`** — 不是 `GetType()` + --- ## Phase 3:现代化深化(3-4 周) From 429b7eaab763952fdd7d9774152be4b1191e086e Mon Sep 17 00:00:00 2001 From: awatercolorpen Date: Thu, 23 Apr 2026 18:08:46 +0800 Subject: [PATCH 4/5] docs: record PR #31 round-2 review findings in modernization spec (CheckObject not on IService, GetDomain takes no args) --- docs/superpowers/specs/2026-03-10-caskin-modernization.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/superpowers/specs/2026-03-10-caskin-modernization.md b/docs/superpowers/specs/2026-03-10-caskin-modernization.md index fe22265..3b95063 100644 --- a/docs/superpowers/specs/2026-03-10-caskin-modernization.md +++ b/docs/superpowers/specs/2026-03-10-caskin-modernization.md @@ -59,6 +59,7 @@ caskin 是一个 Go 的多域 RBAC 权限管理库,基于 casbin 开发。最 - 权限检查 - 前端/后端权限分离 - ⚠️ Review 修复(2026-04-22):4 个编译阻断 bug 已修复(见下方经验积累) + - ⚠️ Review Round 2(2026-04-23):2 个新编译阻断 bug 已修复(见下方经验积累) - [x] 架构说明文档(给贡献者看,docs/architecture.md) - [x] CONTRIBUTING.md @@ -84,6 +85,11 @@ caskin 是一个 Go 的多域 RBAC 权限管理库,基于 casbin 开发。最 3. **`IService` 不暴露 `GetEnforcer()`** — 使用 `svc.CheckObject(user, domain, obj, action) == nil` 替代 `caskin.Check(svc.GetEnforcer(), ...)`;`caskin.Check` 需要具体的 enforcer,不能通过 `IService` 调用 4. **`caskin.Object` 接口方法是 `GetObjectType()`** — 不是 `GetType()` +**caskin API 文档示例追加注意事项(2026-04-23 from PR #31 Round 2):** + +5. **`IService` 没有 `CheckObject(user, domain, obj, action)` 方法** — `CheckObject` 只在 `*server` struct 上,**不在 `IService` 接口里**。所有 `svc.CheckObject(...)` 调用都会编译失败。对 `ObjectData` 类型可用 `svc.CheckModifyObjectData(user, domain, objData)` 等方法;对纯 `Object` 的权限检查需确认正确的公开 API。 +6. **`IService.GetDomain()` 无参数** — 接口签名是 `GetDomain() ([]Domain, error)`,不接受任何参数。要列出特定用户所在的域,使用 `GetDomainByUser(user User) ([]Domain, error)`。 + --- ## Phase 3:现代化深化(3-4 周) From bfa2ea3f1ced278b39b1b5301031e906e09d9160 Mon Sep 17 00:00:00 2001 From: awatercolorpen Date: Fri, 24 Apr 2026 13:04:46 +0800 Subject: [PATCH 5/5] fix(docs/examples): replace all IService.CheckObject calls with GetObject filter pattern CheckObject exists on *server struct but is NOT exposed in the IService interface. All example code that called svc.CheckObject(...) would fail to compile when svc is typed as IService. Fix approach: switch to caskin's query-oriented pattern \u2014 GetObject(user, domain, action) only returns objects the caller may act on; containsObj() then checks whether the target object is present. This is idiomatic caskin and works through IService. Additional fix: GetDomain() takes no arguments \u2014 removed the spurious 'superadmin' argument in Scenario 1 (interface signature is GetDomain() ([]Domain, error)). Refs PR #31 round-3 review notes. --- docs/examples.md | 81 +++++++++++++------ .../specs/2026-03-10-caskin-modernization.md | 5 ++ 2 files changed, 63 insertions(+), 23 deletions(-) diff --git a/docs/examples.md b/docs/examples.md index 3789c9f..ecaef81 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -119,7 +119,8 @@ func multiDomainExample() { fmt.Println("eng admin sees marketing roles:", len(rolesSeenByEngAdmin)) // 0 // The superadmin can see all domains. - domains, _ := svc.GetDomain(superadmin) + // GetDomain takes no arguments — it always returns all domains visible to the caller. + domains, _ := svc.GetDomain() fmt.Println("total domains:", len(domains)) // 2 } ``` @@ -210,15 +211,21 @@ func roleInheritanceExample() { []*caskin.UserRolePair{{User: carol, Role: owner}}) // --- Verify that Bob (editor) inherits viewer permissions --- - // Bob should be able to read (inherited) and write (direct). - // IService exposes CheckObject which returns nil on success. - canRead := svc.CheckObject(bob, domain, resource, caskin.Read) == nil - canWrite := svc.CheckObject(bob, domain, resource, caskin.Write) == nil + // caskin's access-control model is query-oriented: GetObject returns only the + // objects the caller may perform the requested action on. If the resource + // appears in the list the caller is permitted; if it is absent, they are not. + // (containsObj is defined at the bottom of this file) + bobReadObjects, _ := svc.GetObject(bob, domain, caskin.Read) + bobWriteObjects, _ := svc.GetObject(bob, domain, caskin.Write) + + canRead := containsObj(bobReadObjects, resource.GetID()) + canWrite := containsObj(bobWriteObjects, resource.GetID()) fmt.Println("bob can read:", canRead) // true (inherited from viewer) fmt.Println("bob can write:", canWrite) // true (direct on editor) // Alice (viewer) cannot write. - aliceWrite := svc.CheckObject(alice, domain, resource, caskin.Write) == nil + aliceWriteObjects, _ := svc.GetObject(alice, domain, caskin.Write) + aliceWrite := containsObj(aliceWriteObjects, resource.GetID()) fmt.Println("alice can write:", aliceWrite) // false // --- Remove the editor → viewer link at runtime --- @@ -241,8 +248,9 @@ func roleInheritanceExample() { caskin exposes two layers for checking permissions: -1. **`IService.CheckObject`** — service-level check; returns a typed error and - respects the caller's own permission scope. +1. **`IService.GetObject(user, domain, action)`** — query-oriented check; returns only + the objects the caller may perform the action on. If the object is absent from + the result the caller is denied. 2. **`ICurrentService.Check*WithCurrent`** — middleware pattern; binds user and domain once via `SetCurrent` then checks without re-passing them. @@ -282,23 +290,26 @@ func permissionCheckExample() { []*caskin.Policy{{Role: editor, Object: article, Domain: domain, Action: caskin.Write}}, ) - // --- Method 1: service-level boolean check --- - ok := svc.CheckObject(alice, domain, article, caskin.Write) == nil - fmt.Println("alice can write:", ok) // true + // --- Method 1: query-based permission check --- + // GetObject returns only the objects the caller may act on. + // Presence in the list means the permission is granted. + writeObjs, _ := svc.GetObject(alice, domain, caskin.Write) + hasWrite := containsObj(writeObjs, article.GetID()) + fmt.Println("alice can write:", hasWrite) // true - // --- Method 2: service-level check (respects caller's own permissions) --- - err := svc.CheckObject(alice, domain, article, caskin.Write) - fmt.Println("alice write error:", err) // + manageObjs, _ := svc.GetObject(alice, domain, caskin.Manage) + hasManage := containsObj(manageObjs, article.GetID()) + fmt.Println("alice can manage:", hasManage) // false - err = svc.CheckObject(alice, domain, article, caskin.Manage) - fmt.Println("alice manage error:", err) // "no manage permission" + // containsObj is a small helper used throughout this file. + // (defined at the bottom of the examples) // --- Using ICurrentService for middleware-style checks --- // Bind the current user + domain once (e.g. in an HTTP middleware) and // then call the Check* methods without passing user/domain on every call. current := svc.SetCurrent(alice, domain) - err = current.CheckModifyObjectDataWithCurrent(editor) - fmt.Println("alice modify editor (current):", err) // — alice is editor + checkErr := current.CheckModifyObjectDataWithCurrent(editor) + fmt.Println("alice modify editor (current):", checkErr) // — alice is editor } ``` @@ -307,8 +318,9 @@ func permissionCheckExample() { | Scenario | Recommended API | |---|---| | HTTP middleware / auth gate | `ICurrentService.Check*WithCurrent` after `SetCurrent` | -| Business logic, needs typed error | `IService.CheckObject` / `CheckObjectData` | -| Testing policy directly (with concrete `*server`) | `caskin.Check(enforcer, ...)` (package-level, not on `IService`) | +| Object-level permission (query style) | `IService.GetObject(user, domain, action)` — object absent = denied | +| ObjectData permission (roles, etc.) | `IService.CheckModifyObjectData(user, domain, objectData)` | +| Package-level check (internal use) | `caskin.Check(enforcer, user, domain, obj, action)` — not on `IService` | --- @@ -434,10 +446,13 @@ func frontendBackendSeparationExample() { // --- Simulate what an API gateway checks per request --- // "Can Alice call POST /api/users?" - canPost := svc.CheckObject(alice, domain, apiUsersWrite, caskin.Write) == nil + // Use GetObject with the Write action; if apiUsersWrite is absent, Alice is denied. + aliceWriteObjs, _ := svc.GetObject(alice, domain, caskin.Write) + canPost := containsObj(aliceWriteObjs, apiUsersWrite.GetID()) fmt.Println("alice can POST /api/users:", canPost) // false - canPost = svc.CheckObject(bob, domain, apiUsersWrite, caskin.Write) == nil + bobWriteObjs, _ := svc.GetObject(bob, domain, caskin.Write) + canPost = containsObj(bobWriteObjs, apiUsersWrite.GetID()) fmt.Println("bob can POST /api/users:", canPost) // true } @@ -448,6 +463,16 @@ func mustGetObjects(svc caskin.IService, u caskin.User, d caskin.Domain, a caski return objs } +// containsObj returns true if any object in objs has the given ID. +func containsObj(objs []caskin.Object, id uint64) bool { + for _, o := range objs { + if o.GetID() == id { + return true + } + } + return false +} + func filterByType(objs []caskin.Object, ty caskin.ObjectType) []caskin.Object { var out []caskin.Object for _, o := range objs { @@ -477,7 +502,17 @@ func PermissionMiddleware(svc caskin.IService) Middleware { domain := ctx.CurrentDomain().(caskin.Domain) object := lookupObjectForRoute(ctx.Route()).(caskin.Object) - if svc.CheckObject(user, domain, object, caskin.Read) != nil { + // GetObject returns only objects the caller may act on. + // If the specific object is absent from the result the request is denied. + allowed, _ := svc.GetObject(user, domain, caskin.Read) + permitted := false + for _, o := range allowed { + if o.GetID() == object.GetID() { + permitted = true + break + } + } + if !permitted { ctx.Abort(http.StatusForbidden) return } diff --git a/docs/superpowers/specs/2026-03-10-caskin-modernization.md b/docs/superpowers/specs/2026-03-10-caskin-modernization.md index 3b95063..b0816d9 100644 --- a/docs/superpowers/specs/2026-03-10-caskin-modernization.md +++ b/docs/superpowers/specs/2026-03-10-caskin-modernization.md @@ -60,6 +60,7 @@ caskin 是一个 Go 的多域 RBAC 权限管理库,基于 casbin 开发。最 - 前端/后端权限分离 - ⚠️ Review 修复(2026-04-22):4 个编译阻断 bug 已修复(见下方经验积累) - ⚠️ Review Round 2(2026-04-23):2 个新编译阻断 bug 已修复(见下方经验积累) + - ✅ Review Round 3(2026-04-24):Round 2 两个 bug 已修复 — `CheckObject` 全部替换为 `GetObject` 过滤模式,`GetDomain(superadmin)` 改为 `GetDomain()` - [x] 架构说明文档(给贡献者看,docs/architecture.md) - [x] CONTRIBUTING.md @@ -90,6 +91,10 @@ caskin 是一个 Go 的多域 RBAC 权限管理库,基于 casbin 开发。最 5. **`IService` 没有 `CheckObject(user, domain, obj, action)` 方法** — `CheckObject` 只在 `*server` struct 上,**不在 `IService` 接口里**。所有 `svc.CheckObject(...)` 调用都会编译失败。对 `ObjectData` 类型可用 `svc.CheckModifyObjectData(user, domain, objData)` 等方法;对纯 `Object` 的权限检查需确认正确的公开 API。 6. **`IService.GetDomain()` 无参数** — 接口签名是 `GetDomain() ([]Domain, error)`,不接受任何参数。要列出特定用户所在的域,使用 `GetDomainByUser(user User) ([]Domain, error)`。 +**caskin API 文档示例追加注意事项(2026-04-24 from PR #31 Round 3):** + +7. **`IService` 无 `CheckObject` — 使用 `GetObject` 查询模式替代** — caskin 设计为查询导向:`GetObject(user, domain, action)` 只返回调用者有权限访问的对象。判断权限的正确方式是:取得列表后检查目标 object 是否在列表中(`containsObj`)。`CheckObject` 存在于 `*server` struct 上,但不在 `IService` 接口中暴露。 + --- ## Phase 3:现代化深化(3-4 周)