Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 5 additions & 0 deletions cookbook/casbin/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"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,6 +29,8 @@ 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"
Expand All @@ -53,6 +56,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 +66,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
22 changes: 21 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,23 @@ 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('echojwt.JWT(')));
assert.ok(!fences.some((fence) => /^package main\b/m.test(fence.value)), 'the full server is linked rather than duplicated');
}
});
26 changes: 10 additions & 16 deletions site/src/components/HomeHero.astro
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
---
// 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');
---

<section class="hh">
Expand All @@ -28,19 +32,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" />
Comment thread
vishr marked this conversation as resolved.
Outdated
<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 +71,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
115 changes: 5 additions & 110 deletions site/src/content/docs/es/middleware/casbin-auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,58 +39,19 @@ 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 = _, _

[policy_effect]
e = some(where (p.eft == allow))

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

Crea un archivo de policy Casbin `auth_policy.csv`:

```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
```csv file=cookbook/casbin/auth_policy.csv
```

La autenticación y la autorización son responsabilidades separadas. Autentica al usuario con
Expand All @@ -99,16 +60,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 +95,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