https://github.com/theirish81/echosec
A Golang middleware for the Labstack Echo Server that simplifies the process of describing the prerequisites for a request to access combinations of endpoints and methods
https://github.com/theirish81/echosec
golang golang-library labstack-echo middleware
Last synced: 7 months ago
JSON representation
A Golang middleware for the Labstack Echo Server that simplifies the process of describing the prerequisites for a request to access combinations of endpoints and methods
- Host: GitHub
- URL: https://github.com/theirish81/echosec
- Owner: theirish81
- License: mit
- Created: 2024-01-10T15:44:10.000Z (about 2 years ago)
- Default Branch: main
- Last Pushed: 2024-09-20T16:51:09.000Z (over 1 year ago)
- Last Synced: 2024-10-11T20:11:15.204Z (over 1 year ago)
- Topics: golang, golang-library, labstack-echo, middleware
- Language: Go
- Homepage:
- Size: 30.3 KB
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# EchoSec
**EchoSec** is a Golang middleware for the [Labstack Echo Server](https://echo.labstack.com/) that simplifies the process of enforcing request prerequisites across endpoint-method combinations. It uses an OpenAPI extension to direct routing and custom Go functions to define access rules.
Its primary use case is offloading repetitive, boilerplate security logic to middleware. On top of this, EchoSec introduces an advanced *request labeling* system, allowing you to define rules that dynamically assign labels to a request context when matched.
> **Example:** *Can a user with a given JWT token perform `DELETE /user/:id`?*
---
## OpenAPI Integration
Start by annotating your OpenAPI file with the custom `x-echosec` tag. Any operation requiring EchoSec enforcement must include this annotation.
### Core: Functions
```yaml
"/user/{userId}":
get:
summary: |-
Returns the user details. Use `/me` to get the current user. Users can only fetch their own details; admins can see everything.
operationId: getUser
x-echosec:
function: can_admin_user
```
The `x-echosec.function` key defines an identifier that maps to a Go function. While the string is arbitrary, we recommend defining all access modes up front and assigning them consistently.
In this example, `can_admin_user` refers to a function that verifies the requesting user's ability to manage the targeted user.
---
### Parameters
You can further specify behavior using `params`. These are simple strings passed to the validation function to distinguish sub-cases.
```yaml
"/workspace/{workspaceId}/_compute-organizations":
post:
operationId: computeWorkspaceOrganizations
x-echosec:
function: workspace_user_read
params:
- premium
```
Here, the base function `workspace_user_read` applies, but the `premium` param signals a more specific behavior to enforce.
---
### Labels
You can assign labels to the request context using conditional expressions:
```yaml
"/workspace/{workspaceId}/_compute-organizations":
post:
operationId: computeWorkspaceOrganizations
x-echosec:
function: workspace_user_read
params:
- premium
labels:
- label: "agent"
condition: '"agent" in claims.Roles'
```
The condition syntax follows the [Expr](https://github.com/expr-lang/expr) language.
---
## Go Code
### Function Mapping
First, configure your validation logic:
```go
cfg, _ := echosec.NewOApiConfig(openApiBytes, map[string]echosec.OApiValidationFunc{
// Example functions—adapt to your actual claims and access model.
"can_admin_user": func(c echo.Context, params []string) error {
if GetClaims(c).CanAdminUserData(c.Param("userId")) {
return nil
}
return errors.NewForbiddenError()
},
"workspace_user_read": func(ctx echo.Context, params []string) error {
claims := GetClaims(ctx)
if claims.Admin {
return nil
}
if slices.Contains(params, "premium") && !claims.IsPremiumWorkspace(ctx.Param("workspaceId")) {
return errors.NewForbiddenError()
}
if claims.CanAccessWorkspace(ctx.Param("workspaceId")) {
return nil
}
return errors.NewForbiddenError()
},
}, true)
```
Here, the `x-echosec.function` values in the OpenAPI spec are mapped to actual Go functions. These should return `nil` if access is granted, or an error if denied.
* `openApiBytes` is your OpenAPI spec in serialized form (either plain text or gzipped).
* The final `true` argument enables **request validation**. If enabled, malformed requests will be rejected before reaching your access rules.
---
### Labels in Go
Label conditions rely on data extracted from the Echo context. You can promote certain values to the top-level expression scope with `.WithVars()`:
```go
cfg.WithVars("Claims")
```
This allows your label conditions to access `Claims` directly.
The evaluated labels are stored in the `EchoSecContext`, which is made available via:
```go
ectx.Get("echosecContext")
// or
ctx.Value("echosecContext")
```
The `EchoSecContext` struct looks like this:
```go
type EchoSecContext struct {
Config OApiEchoSec
Labels []string
}
```
* `Config` refers to the matched security configuration.
* `Labels` contains the list of active labels assigned to the request.
---
## Response Validation (Optional)
EchoSec also supports validating responses against the OpenAPI spec. This ensures your application complies with the expected schema.
> **Note:** Request validation must be enabled for this to work.
To validate a response, use `SecBlob` instead of `ctx.JSON` or `ctx.Blob`:
```go
echosec.SecBlob(ctx, http.StatusOK, map[string]any{
"a": "foobar",
"b": 1,
})
```
* The first parameter is the Echo context.
* The second is the HTTP status code.
* The third is the response payload (map, string, or `[]byte`).
If the payload doesn’t match the OpenAPI schema, EchoSec returns an error.
---
## Putting It All Together
Using EchoSec is as simple as plugging it in as a middleware:
```go
server := echo.New()
server.Use(echosec.WithOpenApiConfig(cfg))
```
---