package main
import (
"fmt"
"log"
"net/http"
"github.com/thuongh2/go-security/authn"
"github.com/thuongh2/go-security/authz"
"github.com/thuongh2/go-security/core"
"github.com/thuongh2/go-security/crypto/password"
"github.com/thuongh2/go-security/web/middleware"
)
func main() {
// (1) Phase 1 authentication wiring: encoder, users, manager.
encoder := password.NewDelegatingEncoder(password.IDBcrypt)
users := authn.NewInMemoryUserService()
users.Add(authn.User{
Username: "user",
PasswordHash: mustEncode(encoder, "userpw"),
Authorities: core.Roles("USER"),
})
users.Add(authn.User{
Username: "staff",
PasswordHash: mustEncode(encoder, "staffpw"),
Authorities: core.Roles("STAFF"),
})
users.Add(authn.User{
Username: "admin",
PasswordHash: mustEncode(encoder, "adminpw"),
Authorities: core.Roles("ADMIN"),
})
manager := authn.NewManager([]authn.Provider{
authn.NewPasswordProvider(users, encoder),
})
// (2) Role hierarchy: holding ROLE_ADMIN implies ROLE_STAFF and ROLE_USER;
// holding ROLE_STAFF implies ROLE_USER. Constructed once; cycle-checked.
hierarchy, err := authz.ParseRoleHierarchy("ADMIN > STAFF > USER")
if err != nil {
log.Fatalf("role hierarchy: %v", err)
}
// (3) First-match-wins authorization rules. The hierarchy is applied to every
// role rule, so a HasRole("USER") check (implied by /api/** being open to any
// authenticated user here) is satisfied by an ADMIN or STAFF caller too.
//
// AnyRequest().DenyAll() makes the chain default-deny: Routes() would error if
// no terminal catch-all (or AllowUnmatched()) were declared, so the framework
// fails loud rather than fail open.
authzMW, err := authz.Routes(
authz.WithHierarchy(hierarchy),
authz.Match("/admin/**").HasRole("ADMIN"),
authz.Match("/api/**").Authenticated(),
authz.Match("/public/**").PermitAll(),
authz.AnyRequest().DenyAll(),
)
if err != nil {
log.Fatalf("authz routes: %v", err)
}
// A service-layer guard, the @PreAuthorize equivalent. It maps a denial to a
// wrapped sentinel: core.ErrUnauthenticated (anonymous) or core.ErrAccessDenied
// (authenticated), so an HTTP boundary can branch 401 vs 403 with errors.Is.
requireAdmin := authz.Guard(authz.HasRole("ADMIN"), authz.GuardWithHierarchy(hierarchy))
// Application routes. None of these handlers repeat the authorization checks;
// the route middleware enforced them before the handler ran.
mux := http.NewServeMux()
mux.HandleFunc("/public/info", func(w http.ResponseWriter, _ *http.Request) {
fmt.Fprintln(w, "public: open to everyone")
})
mux.HandleFunc("/api/me", func(w http.ResponseWriter, r *http.Request) {
auth := core.MustAuthentication(r.Context())
fmt.Fprintf(w, "name=%s authorities=%v\n", auth.Name(), auth.Authorities())
})
mux.HandleFunc("/admin/panel", func(w http.ResponseWriter, r *http.Request) {
// Belt-and-braces: the same guard can run at the service layer.
if err := requireAdmin(r.Context(), nil); err != nil {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
fmt.Fprintln(w, "admin: you have ROLE_ADMIN")
})
// (4) Compose: headers -> authenticate -> authorize -> routes.
chain := middleware.New(
middleware.SecurityHeaders(),
middleware.BasicAuth(manager, "go-security authz demo"),
authzMW,
)
handler := chain.Then(mux)
addr := ":8080"
log.Printf("listening on %s", addr)
if err := http.ListenAndServe(addr, handler); err != nil {
log.Fatal(err)
}
}
func mustEncode(enc password.Encoder, raw string) string {
h, err := enc.Encode(raw)
if err != nil {
panic(err)
}
return h
}