Quick start
This guide adds wardn to an existing Go web service that uses chi and PostgreSQL. By the end, a route is protected, your first administrator is set up, and you have a test proving it works.
-
Add wardn to your module
Terminal window go get github.com/MogRex-Limited/wardn -
Describe your product in
wardn.yamlPut the file next to your
main.go. It lists where roles apply (scopes), what your code checks (permissions), and the roles that bundle them.wardn.yaml scopes:organisation: {}branch: { parent: organisation }permissions:customer:view: View customersloan:create: Create loan applicationsloan:approve: Approve loansroles:super_admin:assignable_at: [global]allow: ["*"]org_admin:assignable_at: [organisation]allow: ["*"]teller:assignable_at: [branch]allow: [customer:view, loan:create]global(insuper_admin’sassignable_at) is the scope above everything, which every product has without declaring it. A role assigned there applies everywhere.Every permission needs a short description: admin screens show it to people choosing permissions. Two management permissions,
wardn:roles.assignandwardn:roles.manage, are built in and always available."*"grants everything, including them. -
Start wardn in
mainmain.go import ("github.com/MogRex-Limited/wardn"wardnsql "github.com/MogRex-Limited/wardn/store/sql")func main() {db := openDatabase() // your existing *sql.DBw := wardn.MustNew(wardn.Config{File: "wardn.yaml",Store: wardnsql.MustNew(db),})// ...}MustNewvalidates the policy and creates wardn’s tables. If anything inwardn.yamlis wrong, the app stops here with a precise message. -
Tell wardn who the user is
wardn doesn’t log people in. Your existing authentication middleware puts the user’s ID in the request context:
auth.go func authenticate(next http.Handler) http.Handler {return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {if userID, ok := verifyToken(r); ok { // your coder = r.WithContext(wardn.WithUser(r.Context(), userID))}next.ServeHTTP(w, r)})}Set the user only when the token is valid. With no valid token there’s no user, and wardn answers
401. -
Protect your routes
routes.go r := chi.NewRouter()r.Use(authenticate)r.Route("/branches/{branchID}", func(r chi.Router) {r.Use(w.ScopeFrom("branch", wardn.PathValue("branchID")))r.With(w.Require("customer:view")).Get("/customers", listCustomers)r.With(w.Require("loan:create")).Post("/loans", createLoan)r.With(w.Require("loan:approve")).Post("/loans/{id}/approve", approveLoan)})A user without the permission gets
403. A request with no user gets401. Your handler never runs for either. -
Create the first administrator
Nobody holds any role yet, so seed one.
System()makes changes without a signed-in user, for setup scripts and jobs:err := w.System().Assign(ctx, "user-001", "super_admin", wardn.Global) -
Prove it with a test
wardntestruns your real policy in memory, with no database.AsRoleIncreates a user with the role and puts them in the request’s context, just asauthenticatewould. That works becauseauthenticateonly sets a user when a token is present:routes_test.go func TestTellersCannotApproveLoans(t *testing.T) {w := wardntest.New(t, "wardn.yaml")router := newRouter(w)req := httptest.NewRequest("POST", "/branches/lagos/loans/7/approve", nil)req = wardntest.AsRoleIn(t, w, req, wardn.NewScope("branch", "lagos"), "teller")rec := httptest.NewRecorder()router.ServeHTTP(rec, req)if rec.Code != http.StatusForbidden {t.Fatalf("status %d, want 403", rec.Code)}}
That’s it: roles defined, tables created, routes protected, first admin seeded, and a test that fails if anyone breaks it.
What just happened
Section titled “What just happened”- The policy was validated at startup. Unknown permissions, misspelled scope types and invalid names would have stopped the app, with the file, line and a suggestion.
ScopeFromset where the request acts (branchlagos). An organisation admin of the organisation that ownslagosis allowed there automatically, once wardn knows thatlagosbelongs to it (see Scopes and hierarchy).Requirechecked the permission against the user’s roles at that scope and every scope above it, then let the request through or stopped it.
Next steps
Section titled “Next steps”Questions about this page?
Ask below, point out something wrong or unclear, or suggest an improvement. Reading needs no account; to post, sign in with GitHub.