Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions cookbook/casbin/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ package main
import (
"log/slog"
"net/http"
"os"

"github.com/casbin/casbin/v3"
"github.com/golang-jwt/jwt/v5"
echojwt "github.com/labstack/echo-jwt/v5"
"github.com/labstack/echo/v5"
)

// docs:start middleware
// NewCasbinMiddleware returns middleware for [Casbin](https://casbin.org/).
func NewCasbinMiddleware(enforcer *casbin.Enforcer, userGetter func(*echo.Context) (string, error)) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
Expand All @@ -28,17 +30,22 @@ func NewCasbinMiddleware(enforcer *casbin.Enforcer, userGetter func(*echo.Contex
}
}

// docs:end middleware

/*
Test with:
curl -v "http://localhost:8080/dataset1/any" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"
*/
func main() {
e := echo.New()

// docs:start enforcer
ce, err := casbin.NewEnforcer("auth_model.conf", "auth_policy.csv")
if err != nil {
slog.Error("failed to initialize Casbin enforcer", "error", err)
os.Exit(1)
}
// docs:end enforcer

// BasicAuth middleware does authentication
// - should pass `curl -v -u "alice:password" http://localhost:8080/dataset1/any`
Expand All @@ -53,6 +60,7 @@ func main() {
//}
//e.Use(NewCasbinMiddleware(ce, basicAuthUser)) // Casbin does authorization

// docs:start jwt
e.Use(echojwt.JWT([]byte("secret"))) // JWT middleware does authentication
jwtUser := func(c *echo.Context) (string, error) { // JWT user getter for Casbin authorization
token, err := echo.ContextGet[*jwt.Token](c, "user")
Expand All @@ -62,6 +70,7 @@ func main() {
return token.Claims.GetSubject()
}
e.Use(NewCasbinMiddleware(ce, jwtUser)) // Casbin does authorization
// docs:end jwt

e.GET("/*", func(c *echo.Context) error {
return c.String(http.StatusOK, "Hello, World!")
Expand Down
2 changes: 2 additions & 0 deletions cookbook/hello-world/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

func main() {
// Echo instance
// docs:start hero
e := echo.New()

// Middleware
Expand All @@ -20,6 +21,7 @@ func main() {
e.GET("/", func(c *echo.Context) error {
return c.String(http.StatusOK, "Hello, World!\n")
})
// docs:end hero

// Start server
sc := echo.StartConfig{Address: ":1323"}
Expand Down
7 changes: 6 additions & 1 deletion site/plugins/remark-source-code.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,12 @@ function readSnippet(specifier, root) {
} catch (error) {
throw new Error(`Cannot include ${path}: ${error.message}`);
}
const lines = source.split('\n');
return sourceSnippet(source, region, path);
}

/** Extract the same source regions from Vite raw imports and Markdown includes. */
export function sourceSnippet(source, region, path = 'source') {
const lines = source.replace(/\r\n/g, '\n').split('\n');
if (region !== undefined) {
const starts = [];
const ends = [];
Expand Down
23 changes: 22 additions & 1 deletion site/plugins/remark-source-code.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { join } from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';
import { unified } from 'unified';
import remarkSourceCode, { sourceReference, withSourceCode } from './remark-source-code.mjs';
import remarkSourceCode, { sourceReference, sourceSnippet, withSourceCode } from './remark-source-code.mjs';
import { parseSourceDocument, visitCode } from './source-document.mjs';
import { localePrefixes } from '../src/locales.mjs';

Expand Down Expand Up @@ -212,3 +212,24 @@ test('every cookbook page in every locale displays source-owned programs and exc
}
}
});

test('raw component imports and Markdown includes share source excerpts; Casbin middleware appears once per locale', () => {
const root = fileURLToPath(new URL('../..', import.meta.url));
const path = 'cookbook/hello-world/server.go';
const code = sourceSnippet(readFileSync(join(root, path), 'utf8'), 'hero', path);
const [included] = programFences(render(`\x60\x60\x60go file=${path}#hero\n\x60\x60\x60`, root));
assert.equal(code, included.value);
assert.match(code, /e\.GET\("\/"/);
assert.doesNotMatch(code, /docs:(start|end)/);
for (const locale of localePrefixes) {
const page = join(root, 'site/src/content/docs', locale, 'middleware/casbin-auth.md');
const rendered = render(readFileSync(page, 'utf8'), root, page);
const fences = programFences(rendered);
const declarations = fences.flatMap((fence) => [...fence.value.matchAll(/^func NewCasbinMiddleware\b/gm)]);
assert.equal(declarations.length, 1, `${locale}Casbin middleware has a single source-backed definition`);
assert.ok(fences.some((fence) => fence.value.includes('enforcer.Enforce(')));
assert.ok(fences.some((fence) => fence.value.includes('casbin.NewEnforcer(')), `${locale}the enforcer used by the excerpts is defined`);
assert.ok(fences.some((fence) => fence.value.includes('echojwt.JWT(')));
assert.ok(!fences.some((fence) => /^package main\b/m.test(fence.value)), 'the full server is linked rather than duplicated');
}
});
28 changes: 12 additions & 16 deletions site/src/components/HomeHero.astro
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
---
// Living Terminal hero: code window + a terminal pane that types `curl`
// and streams Echo's JSON response. Animation is client-side, with a
// static final-state fallback for reduced-motion / no-JS.
// and shows the runnable example's response. Animation is client-side, with a
// static final-state fallback for reduced motion.
import { starsLabel } from '../data/github.ts';
import stable from '../generated/echo-source.json';
import { Code } from 'astro:components';
import helloWorldSource from '../../../cookbook/hello-world/server.go?raw';
import { sourceSnippet } from '../../plugins/remark-source-code.mjs';
const next = process.env.DOCS_CHANNEL === 'next';
const helloWorldCode = sourceSnippet(helloWorldSource, 'hero', 'cookbook/hello-world/server.go');
// The code window is aria-hidden, so Shiki's <pre> must not be a tab stop.
const notFocusable = { pre(node: { properties: Record<string, unknown> }) { delete node.properties.tabindex; } };
---

<section class="hh">
Expand All @@ -28,19 +34,9 @@ const next = process.env.DOCS_CHANNEL === 'next';
<div class="hh-win" aria-hidden="true">
<div class="bar">
<i style="background:#ff5f57"></i><i style="background:#febc2e"></i><i style="background:#28c840"></i>
<span class="fn">main.go</span>
<span class="fn">server.go · excerpt</span>
</div>
<pre><span class="kw">package</span> main

<span class="kw">import</span> <span class="str">"github.com/labstack/echo/v5"</span>

<span class="kw">func</span> <span class="fnn">main</span>() &#123;
e := echo.<span class="fnn">New</span>()
e.<span class="fnn">GET</span>(<span class="str">"/"</span>, <span class="kw">func</span>(c *echo.Context) <span class="kw">error</span> &#123;
<span class="kw">return</span> c.<span class="fnn">JSON</span>(<span class="str">200</span>, <span class="kw">map</span>[<span class="kw">string</span>]<span class="kw">string</span>&#123;<span class="str">"message"</span>: <span class="str">"Hello, World!"</span>&#125;)
&#125;)
e.<span class="fnn">Start</span>(<span class="str">":1323"</span>)
&#125;</pre>
<Code code={helloWorldCode} lang="go" style="background: transparent" transformers={[notFocusable]} />
<div class="hh-term" data-term>
<div><span class="pr">~/echo $</span> <span data-cmd></span><span class="hh-cur" data-cur></span></div>
<div data-out></div>
Expand Down Expand Up @@ -77,8 +73,8 @@ const next = process.env.DOCS_CHANNEL === 'next';
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

const response =
'<span class="dim">HTTP/1.1 200 OK · 0 allocations</span>\n' +
'<span class="json">{ "message": "Hello, World!" }</span>';
'<span class="dim">HTTP/1.1 200 OK</span>\n' +
'<span class="json">Hello, World!</span>';
const cmd = 'curl localhost:1323';

const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
Expand Down
122 changes: 13 additions & 109 deletions site/src/content/docs/es/middleware/casbin-auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,15 @@ Consulta el [resumen de API](https://casbin.org/docs/api-overview) y la

```bash
go get github.com/casbin/casbin/v3
go get github.com/labstack/echo-jwt/v5
go get github.com/golang-jwt/jwt/v5
```

```go
import (
"github.com/casbin/casbin/v3"
"github.com/golang-jwt/jwt/v5"
echojwt "github.com/labstack/echo-jwt/v5"
)
```

Expand All @@ -39,58 +43,24 @@ import (
Echo no incluye un middleware Casbin; la integración es un wrapper pequeño alrededor del
enforcer de Casbin:

```go
// NewCasbinMiddleware returns middleware for Casbin (https://casbin.org/).
func NewCasbinMiddleware(enforcer *casbin.Enforcer, userGetter func(*echo.Context) (string, error)) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c *echo.Context) error {
username, err := userGetter(c)
if err != nil {
return echo.ErrUnauthorized.Wrap(err)
}
if pass, err := enforcer.Enforce(username, c.Request().URL.Path, c.Request().Method); err != nil {
return echo.ErrInternalServerError.Wrap(err)
} else if !pass {
return echo.NewHTTPError(http.StatusForbidden, "access denied")
}
return next(c)
}
}
}
```go file=cookbook/casbin/server.go#middleware
```

## Ejemplo

Crea un archivo de modelo Casbin `auth_model.conf`:

```ini
[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[role_definition]
g = _, _
```ini file=cookbook/casbin/auth_model.conf
```

[policy_effect]
e = some(where (p.eft == allow))
Crea un archivo de policy Casbin `auth_policy.csv`:

[matchers]
m = g(r.sub, p.sub) && keyMatch(r.obj, p.obj) && (r.act == p.act || p.act == "*")
```csv file=cookbook/casbin/auth_policy.csv
```

Crea un archivo de policy Casbin `auth_policy.csv`:
Carga el modelo y la policy en un enforcer de Casbin:

```csv
p, 1234567890, /dataset1/*, GET
p, alice, /dataset1/*, GET
p, alice, /dataset1/resource1, POST
p, bob, /dataset2/resource1, *
p, bob, /dataset2/resource2, GET
p, bob, /dataset2/folder1/*, POST
p, dataset1_admin, /dataset1/*, *
g, cathy, dataset1_admin
```go file=cookbook/casbin/server.go#enforcer
```

La autenticación y la autorización son responsabilidades separadas. Autentica al usuario con
Expand All @@ -99,16 +69,7 @@ pueda autorizar el request.

### Con JWT

```go
e.Use(echojwt.JWT([]byte("secret"))) // JWT middleware does authentication
jwtUser := func(c *echo.Context) (string, error) { // JWT user getter for Casbin authorization
token, err := echo.ContextGet[*jwt.Token](c, "user")
if err != nil {
return "", err
}
return token.Claims.GetSubject()
}
e.Use(NewCasbinMiddleware(ce, jwtUser)) // Casbin does authorization
```go file=cookbook/casbin/server.go#jwt
```

Pruébalo con:
Expand Down Expand Up @@ -143,61 +104,4 @@ curl -v -u "alice:password" http://localhost:8080/dataset2/resource2

### Ejemplo completo de Casbin + JWT

```go
package main

import (
"log/slog"
"net/http"

"github.com/casbin/casbin/v3"
"github.com/golang-jwt/jwt/v5"
echojwt "github.com/labstack/echo-jwt/v5"
"github.com/labstack/echo/v5"
)

// NewCasbinMiddleware returns middleware for Casbin (https://casbin.org/).
func NewCasbinMiddleware(enforcer *casbin.Enforcer, userGetter func(*echo.Context) (string, error)) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c *echo.Context) error {
username, err := userGetter(c)
if err != nil {
return echo.ErrUnauthorized.Wrap(err)
}
if pass, err := enforcer.Enforce(username, c.Request().URL.Path, c.Request().Method); err != nil {
return echo.ErrInternalServerError.Wrap(err)
} else if !pass {
return echo.NewHTTPError(http.StatusForbidden, "access denied")
}
return next(c)
}
}
}

func main() {
e := echo.New()

ce, err := casbin.NewEnforcer("auth_model.conf", "auth_policy.csv")
if err != nil {
slog.Error("failed to initialize Casbin enforcer", "error", err)
}

e.Use(echojwt.JWT([]byte("secret"))) // JWT middleware does authentication
jwtUser := func(c *echo.Context) (string, error) { // JWT user getter for Casbin authorization
token, err := echo.ContextGet[*jwt.Token](c, "user")
if err != nil {
return "", err
}
return token.Claims.GetSubject()
}
e.Use(NewCasbinMiddleware(ce, jwtUser)) // Casbin does authorization

e.GET("/*", func(c *echo.Context) error {
return c.String(http.StatusOK, "Hello, World!")
})

if err := e.Start(":8080"); err != nil {
e.Logger.Error("failed to start server", "error", err)
}
}
```
Ejecuta el [ejemplo completo de Casbin + JWT](https://github.com/labstack/echox/tree/master/cookbook/casbin) con los archivos de modelo y política anteriores.
Loading
Loading