Skip to content

docs: add usage examples (multi-domain, role inheritance, permission checks, frontend/backend)#31

Open
AWaterColorPen wants to merge 5 commits intomainfrom
feature/phase2-usage-examples
Open

docs: add usage examples (multi-domain, role inheritance, permission checks, frontend/backend)#31
AWaterColorPen wants to merge 5 commits intomainfrom
feature/phase2-usage-examples

Conversation

@AWaterColorPen
Copy link
Copy Markdown
Owner

Summary

Phase 2 third task: Usage Examples — practical, self-contained code examples for the four most common caskin scenarios.

Changes

  • New docs/examples.md — covers:
    1. Multi-domain management — creating isolated tenants, domain bootstrapping, superadmin cross-domain access, per-domain admin assignment
    2. Role inheritance — building a three-level hierarchy (viewer → editor → owner), granting permissions, verifying inheritance at runtime, removing links
    3. Permission checkscaskin.Check (low-level bool), IService.CheckObject (typed error), and ICurrentService.Check*WithCurrent (middleware pattern)
    4. Frontend / backend permission separation — separate object sub-trees for UI pages and API endpoints, simulating what a login page and an API gateway would query
  • Updated docs/superpowers/specs/2026-03-10-caskin-modernization.md to mark this task done

Phase 2 progress

Copy link
Copy Markdown
Owner Author

@AWaterColorPen AWaterColorPen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The structure and scenario coverage are excellent. Four compilation-blocking issues need fixing before merge.

Comment thread docs/examples.md

// newService creates an in-memory caskin service for demonstration.
func newService() (caskin.IService, *gorm.DB) {
dir, _ := os.MkdirTemp("", "caskin-example-*")
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing caskin.Register[...]() call

All real usages of caskin (playground, tests) call caskin.Register[*example.User, *example.Role, *example.Object, *example.Domain]() before constructing the service. Without it the factory does not know which concrete types to instantiate and panics at runtime. Please add this call to newService() — see the corrected snippet in the comment on caskin.New below.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Rewrote newService() to use caskin.Options{DB: dbOption, Dictionary: &caskin.DictionaryOption{Dsn: "configs/caskin.toml"}} directly, and added caskin.Register[*example.User, *example.Role, *example.Object, *example.Domain]() before caskin.New. Mirrors playground/playground.go exactly.

Comment thread docs/examples.md Outdated
dict, _ := dictOption.Build()

svc, _ := caskin.New(
caskin.WithDB(db),
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: caskin.New does not accept functional options

caskin.WithDB, caskin.WithDictionary, and caskin.DefaultModelText() do not exist in the codebase — this will not compile. caskin.New takes an *Options struct directly, and DictionaryOption only has Dsn/Type fields.

Suggested fix (mirrors playground/playground.go):

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{},
    )
    caskin.Register[*example.User, *example.Role, *example.Object, *example.Domain]()
    svc, _ := caskin.New(&caskin.Options{DB: dbOption})
    return svc, db
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed alongside the Register comment — the whole newService() helper is now rewritten to pass &caskin.Options{DB: dbOption, Dictionary: &caskin.DictionaryOption{Dsn: "configs/caskin.toml"}}. The non-existent WithDB, WithDictionary, and DefaultModelText() helpers are gone.

Comment thread docs/examples.md Outdated
// --- 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)
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: IService has no GetEnforcer() method

caskin.Check(svc.GetEnforcer(), ...) will not compile — IService does not expose the internal enforcer. This pattern is used in Scenarios 2, 3, and 4.

For a public-API-safe alternative:

// instead of:
canRead := caskin.Check(svc.GetEnforcer(), bob, domain, resource, caskin.Read)

// use:
canRead := svc.CheckObject(bob, domain, resource, caskin.Read) == nil

If showing the low-level bool return is important for the example, the prose should note that caskin.Check requires a concrete *server type rather than IService.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. All caskin.Check(svc.GetEnforcer(), ...) calls have been replaced with svc.CheckObject(...) == nil throughout Scenarios 2, 3, and 4 (including the API gateway simulation). The middleware pseudocode now uses svc.CheckObject(...) != nil as the rejection guard. Also updated the intro text and "Choosing the right check" table in Scenario 3 to clarify that caskin.Check requires a concrete enforcer, not IService.

Comment thread docs/examples.md
}

func filterByType(objs []caskin.Object, ty caskin.ObjectType) []caskin.Object {
var out []caskin.Object
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: o.GetType()o.GetObjectType()

The caskin.Object interface defines GetObjectType() string, not GetType(). The filterByType helper will not compile.

// fix:
func filterByType(objs []caskin.Object, ty caskin.ObjectType) []caskin.Object {
    var out []caskin.Object
    for _, o := range objs {
        if o.GetObjectType() == ty {
            out = append(out, o)
        }
    }
    return out
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. filterByType now calls o.GetObjectType() as defined by the caskin.Object interface.

- 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
Copy link
Copy Markdown
Owner Author

@AWaterColorPen AWaterColorPen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two more compilation-blocking bugs found after the first round of fixes.

Comment thread docs/examples.md Outdated
// --- 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
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: IService has no CheckObject method

svc.CheckObject(...) will not compile because CheckObject is defined on the concrete *server type but is not part of the IService interface. svc is typed as caskin.IService, so any call to svc.CheckObject is a compile error.

This affects every CheckObject call in Scenarios 2, 3, and 4 (lines 215, 216, 221, 286, 290, 293, 437, 440, 480).

The public API for permission checks on plain Object items does not appear to be exposed through IService. Recommend verifying against the playground/tests how Object permissions are checked through the service interface, then updating the examples accordingly. One working pattern already shown in the existing codebase is svc.CheckCreateObjectData / svc.CheckModifyObjectData for ObjectData types.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in Round 3 (2026-04-24). All svc.CheckObject(...) calls have been replaced with the query-oriented pattern:

objs, _ := svc.GetObject(user, domain, action)
allowed := containsObj(objs, obj.GetID())

GetObject returns only the objects the caller is permitted to act on, so checking for presence is semantically equivalent to a direct permission check. A containsObj helper is defined at the bottom of the file and reused throughout Scenarios 2, 3, and 4.

Comment thread docs/examples.md Outdated
fmt.Println("eng admin sees marketing roles:", len(rolesSeenByEngAdmin)) // 0

// The superadmin can see all domains.
domains, _ := svc.GetDomain(superadmin)
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: GetDomain takes no arguments

IService.GetDomain() is declared as GetDomain() ([]Domain, error) — it accepts no parameters. The call svc.GetDomain(superadmin) will not compile.

Fix options:

// Fix A: list all domains (GetDomain returns all, superadmin can see all)
domains, _ := svc.GetDomain()

// Fix B: domains the superadmin is enrolled in
domains, _ := svc.GetDomainByUser(superadmin)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in Round 3 (2026-04-24). Changed to svc.GetDomain() with no arguments (Fix A). Since GetDomain() returns all domains visible to the caller, and the superadmin can see all domains, this is the correct call for Scenario 1’s listing step.

…eckObject not on IService, GetDomain takes no args)
…ject 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.
@AWaterColorPen
Copy link
Copy Markdown
Owner Author

Round 3 fix (2026-04-24):

Both compilation-blocking issues from Round 2 have been addressed:

  1. IService has no CheckObject method — all svc.CheckObject(...) calls have been replaced with caskin's idiomatic query-oriented pattern:

    // Instead of: svc.CheckObject(user, domain, obj, action) == nil
    objs, _ := svc.GetObject(user, domain, action)
    allowed := containsObj(objs, obj.GetID())

    GetObject only returns objects the caller may act on, so checking presence is semantically equivalent. containsObj is a small helper defined at the bottom of the file.

  2. GetDomain() takes no arguments — fixed svc.GetDomain(superadmin)svc.GetDomain() in Scenario 1.

The table in "Choosing the right check" has also been updated to reflect the correct public API surface.

Ready for re-review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant