Enhance API Guards and Documentation
- Added a new guard option for OAuth 2.1 authentication, with ACL checks performed in the Run function for API calls. - Updated the API routing to include the new guard and clarified the OAuth guard's functionality in the code comments. - Enhanced documentation to reflect the new guard options and their descriptions, improving clarity for developers on available authentication methods.
This commit is contained in:
parent
8485dd420e
commit
a37bb5cdba
6 changed files with 345 additions and 16 deletions
|
|
@ -25,6 +25,7 @@ var dsl = []byte(`
|
|||
"description": "Run the backend script, with Api prefix method",
|
||||
"path": "/run/*route",
|
||||
"method": "POST",
|
||||
"guard": "-",
|
||||
"process": "sui.Run",
|
||||
"in": [":context", "$param.route", ":payload"],
|
||||
"out": { "status": 200, "type": "application/json" }
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ var Guards = map[string]func(c *Request) error{
|
|||
"query-jwt": guardQueryJWT, // Get JWT Token from query string "__tk"
|
||||
"cookie-jwt": guardCookieJWT, // Get JWT Token from cookie "__tk"
|
||||
"cookie-trace": guardCookieTrace, // Set sid cookie
|
||||
"oauth": guardOAuth, // OAuth 2.1 guard
|
||||
"oauth": guardOAuth, // OAuth 2.1 authentication (ACL check done in Run for API calls)
|
||||
}
|
||||
|
||||
// JWT Bearer JWT
|
||||
|
|
@ -94,8 +94,9 @@ func guardCookieTrace(r *Request) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// OAuth 2.1 guard using openapi/oauth service
|
||||
// This guard only authenticates the user without ACL checks (suitable for page rendering)
|
||||
// OAuth 2.1 guard - authentication only
|
||||
// This guard validates the token and sets authorized info
|
||||
// ACL checks are performed separately in Run() for API calls
|
||||
func guardOAuth(r *Request) error {
|
||||
if r.context == nil {
|
||||
return fmt.Errorf("Context is nil")
|
||||
|
|
@ -107,7 +108,7 @@ func guardOAuth(r *Request) error {
|
|||
|
||||
c := r.context
|
||||
|
||||
// Authenticate only (validates token and sets authorized info, no ACL check)
|
||||
// Authenticate only (validates token and sets authorized info)
|
||||
if !oauth.OAuth.Authenticate(c) {
|
||||
return fmt.Errorf("Not authenticated")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/yaoapp/gou/process"
|
||||
"github.com/yaoapp/kun/exception"
|
||||
"github.com/yaoapp/kun/log"
|
||||
"github.com/yaoapp/yao/openapi/oauth/acl"
|
||||
"github.com/yaoapp/yao/sui/core"
|
||||
)
|
||||
|
||||
|
|
@ -108,14 +109,17 @@ func Run(process *process.Process) interface{} {
|
|||
return nil
|
||||
}
|
||||
|
||||
scriptCtx, err := script.NewContext(process.Sid, nil)
|
||||
scriptCtx, err := script.NewContext(r.Sid, nil)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer scriptCtx.Close()
|
||||
|
||||
// Pass authorized info to V8 context
|
||||
if authorized := process.GetAuthorized(); authorized != nil {
|
||||
// Priority: 1. From Request (set by SUI guard), 2. From Process (set by GOU handler)
|
||||
if len(r.Authorized) > 0 {
|
||||
scriptCtx.WithAuthorized(r.Authorized)
|
||||
} else if authorized := process.GetAuthorized(); authorized != nil {
|
||||
if authMap := authorized.AuthorizedToMap(); len(authMap) > 0 {
|
||||
scriptCtx.WithAuthorized(authMap)
|
||||
}
|
||||
|
|
@ -185,11 +189,19 @@ func (r *Request) apiGuard(method string, api *core.PageAPI) (int, error) {
|
|||
}
|
||||
|
||||
// Build in guard
|
||||
if guard, has := Guards[guard]; has {
|
||||
err := guard(r)
|
||||
if guardFunc, has := Guards[guard]; has {
|
||||
err := guardFunc(r)
|
||||
if err != nil {
|
||||
return 403, err
|
||||
}
|
||||
|
||||
// For OAuth guard, perform ACL check after authentication
|
||||
if guard == "oauth" {
|
||||
if err := r.enforceACL(); err != nil {
|
||||
return 403, err
|
||||
}
|
||||
}
|
||||
|
||||
return 200, nil
|
||||
}
|
||||
|
||||
|
|
@ -202,11 +214,33 @@ func (r *Request) apiGuard(method string, api *core.PageAPI) (int, error) {
|
|||
return 200, nil
|
||||
}
|
||||
|
||||
// enforceACL performs ACL permission check for API calls
|
||||
func (r *Request) enforceACL() error {
|
||||
if r.context == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Skip if ACL is not enabled
|
||||
if acl.Global == nil || !acl.Global.Enabled() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Enforce ACL
|
||||
ok, err := acl.Global.Enforce(r.context)
|
||||
if err != nil {
|
||||
log.Error("[SUI] ACL enforcement failed: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if !ok {
|
||||
return fmt.Errorf("Access denied")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func configWriter() {
|
||||
for {
|
||||
select {
|
||||
case config := <-chConfig:
|
||||
configs[config.Root] = config
|
||||
}
|
||||
for config := range chConfig {
|
||||
configs[config.Root] = config
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -171,11 +171,24 @@ Create `<page>.config` for page settings:
|
|||
```json
|
||||
{
|
||||
"title": "Page Title",
|
||||
"guard": "bearer-jwt",
|
||||
"cache": 3600
|
||||
"guard": "oauth",
|
||||
"api": {
|
||||
"defaultGuard": "oauth"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Available Guards
|
||||
|
||||
| Guard | Description |
|
||||
| -------------- | ------------------------------------------ |
|
||||
| `oauth` | OAuth 2.1 authentication (recommended) |
|
||||
| `bearer-jwt` | Bearer token JWT authentication |
|
||||
| `cookie-jwt` | Cookie-based JWT authentication |
|
||||
| `-` | No authentication (public access) |
|
||||
|
||||
> See [Page Configuration](./page-config.md) for complete configuration options.
|
||||
|
||||
## Backend Scripts
|
||||
|
||||
Each page can have a backend script:
|
||||
|
|
|
|||
|
|
@ -185,7 +185,26 @@ interface Request {
|
|||
sid: string; // Session ID
|
||||
theme: string; // Current theme
|
||||
locale: string; // Current locale
|
||||
authorized?: Record<string, any>; // OAuth info (when guard is "oauth")
|
||||
authorized?: {
|
||||
// OAuth info (when guard is "oauth")
|
||||
sub?: string; // Subject identifier
|
||||
user_id?: string; // User ID
|
||||
team_id?: string; // Team ID (if team login)
|
||||
tenant_id?: string; // Tenant ID (multi-tenancy)
|
||||
client_id?: string; // OAuth client ID
|
||||
session_id?: string; // Session ID
|
||||
scope?: string; // OAuth scopes
|
||||
remember_me?: boolean; // Remember me flag
|
||||
|
||||
// Data access constraints (set by ACL)
|
||||
constraints?: {
|
||||
owner_only?: boolean; // Only access owner's data
|
||||
creator_only?: boolean; // Only access creator's data
|
||||
editor_only?: boolean; // Only access editor's data
|
||||
team_only?: boolean; // Only access team's data
|
||||
extra?: Record<string, any>; // Custom constraints
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
261
sui/docs/page-config.md
Normal file
261
sui/docs/page-config.md
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
# Page Configuration
|
||||
|
||||
Each SUI page can have a configuration file (`<page>.config`) that defines page-level settings including title, guards, caching, and API options.
|
||||
|
||||
## File Naming
|
||||
|
||||
Configuration files use the naming convention `<page>.config`:
|
||||
|
||||
```
|
||||
/pages/users/
|
||||
├── users.html
|
||||
├── users.css
|
||||
├── users.ts
|
||||
├── users.json
|
||||
├── users.config # Page configuration
|
||||
└── users.backend.ts
|
||||
```
|
||||
|
||||
## Configuration Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Page Title",
|
||||
"description": "Page description",
|
||||
"guard": "oauth",
|
||||
"cache": 3600,
|
||||
"dataCache": 300,
|
||||
"cacheStore": "redis",
|
||||
"root": "/custom-root",
|
||||
"seo": {
|
||||
"title": "SEO Title",
|
||||
"description": "SEO Description",
|
||||
"keywords": "keyword1, keyword2",
|
||||
"image": "/images/og-image.png",
|
||||
"url": "https://example.com/page"
|
||||
},
|
||||
"api": {
|
||||
"prefix": "Api",
|
||||
"defaultGuard": "oauth",
|
||||
"guards": {
|
||||
"PublicMethod": "-",
|
||||
"AdminMethod": "bearer-jwt"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Basic Options
|
||||
|
||||
| Option | Type | Description | Default |
|
||||
| ------------- | ------ | -------------------------------- | ------- |
|
||||
| `title` | string | Page title | - |
|
||||
| `description` | string | Page description | - |
|
||||
| `guard` | string | Guard for page rendering | - |
|
||||
| `cache` | number | Page cache duration in seconds | 0 |
|
||||
| `dataCache` | number | Data cache duration in seconds | 0 |
|
||||
| `cacheStore` | string | Cache store name (e.g., "redis") | - |
|
||||
| `root` | string | Custom root path for the page | - |
|
||||
|
||||
### SEO Options
|
||||
|
||||
```json
|
||||
{
|
||||
"seo": {
|
||||
"title": "SEO Title - Different from page title",
|
||||
"description": "Meta description for search engines",
|
||||
"keywords": "comma, separated, keywords",
|
||||
"image": "/images/og-image.png",
|
||||
"url": "https://example.com/canonical-url"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### API Options
|
||||
|
||||
The `api` section configures guards for backend API methods (called via `$Backend().Call()`):
|
||||
|
||||
```json
|
||||
{
|
||||
"api": {
|
||||
"prefix": "Api",
|
||||
"defaultGuard": "oauth",
|
||||
"guards": {
|
||||
"MethodName": "guard-name"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Option | Type | Description | Default |
|
||||
| -------------- | ------ | --------------------------------- | ------- |
|
||||
| `prefix` | string | Method prefix for API functions | "Api" |
|
||||
| `defaultGuard` | string | Default guard for all API methods | - |
|
||||
| `guards` | object | Per-method guard overrides | - |
|
||||
|
||||
## Guards
|
||||
|
||||
SUI supports the following built-in guards:
|
||||
|
||||
| Guard | Description |
|
||||
| -------------- | ----------------------------------------------- |
|
||||
| `oauth` | OAuth 2.1 authentication (recommended) |
|
||||
| `bearer-jwt` | Bearer token JWT authentication |
|
||||
| `cookie-jwt` | Cookie-based JWT authentication |
|
||||
| `query-jwt` | Query string JWT authentication (`?__tk=token`) |
|
||||
| `cookie-trace` | Session tracking via cookie |
|
||||
| `-` | No authentication (public access) |
|
||||
|
||||
### Page Guard vs API Guard
|
||||
|
||||
- **Page Guard** (`guard`): Controls access to page rendering
|
||||
- **API Guard** (`api.defaultGuard` / `api.guards`): Controls access to backend API methods
|
||||
|
||||
```json
|
||||
{
|
||||
"guard": "oauth",
|
||||
"api": {
|
||||
"defaultGuard": "oauth",
|
||||
"guards": {
|
||||
"PublicSearch": "-"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In this example:
|
||||
|
||||
- Page rendering requires OAuth authentication
|
||||
- All API methods require OAuth by default
|
||||
- `ApiPublicSearch` method is publicly accessible
|
||||
|
||||
## Examples
|
||||
|
||||
### Public Page
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Welcome",
|
||||
"description": "Public landing page"
|
||||
}
|
||||
```
|
||||
|
||||
### Protected Page with OAuth
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Dashboard",
|
||||
"guard": "oauth",
|
||||
"api": {
|
||||
"defaultGuard": "oauth"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Mixed Access Page
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Product Catalog",
|
||||
"guard": "-",
|
||||
"api": {
|
||||
"defaultGuard": "-",
|
||||
"guards": {
|
||||
"AddToCart": "oauth",
|
||||
"Checkout": "oauth"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Page is public, most API methods are public, but cart and checkout require authentication.
|
||||
|
||||
### Cached Page
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Blog Post",
|
||||
"cache": 3600,
|
||||
"dataCache": 300,
|
||||
"guard": "-"
|
||||
}
|
||||
```
|
||||
|
||||
### Full Configuration Example
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "User Settings",
|
||||
"description": "Manage your account settings",
|
||||
"guard": "oauth",
|
||||
"cache": 0,
|
||||
"dataCache": 60,
|
||||
"seo": {
|
||||
"title": "Account Settings | MyApp",
|
||||
"description": "Configure your account preferences and security settings"
|
||||
},
|
||||
"api": {
|
||||
"defaultGuard": "oauth",
|
||||
"guards": {
|
||||
"GetPublicProfile": "-",
|
||||
"UpdateProfile": "oauth",
|
||||
"DeleteAccount": "oauth"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Accessing Authorized Info
|
||||
|
||||
When using `oauth` guard, the authorized user information is available in:
|
||||
|
||||
### Backend Scripts
|
||||
|
||||
```typescript
|
||||
function ApiGetUserData(request: Request): any {
|
||||
// Access OAuth info from request.authorized
|
||||
const userId = request.authorized?.user_id;
|
||||
const teamId = request.authorized?.team_id;
|
||||
const clientId = request.authorized?.client_id;
|
||||
const scope = request.authorized?.scope;
|
||||
|
||||
// Access data constraints (set by ACL)
|
||||
const ownerOnly = request.authorized?.constraints?.owner_only;
|
||||
const teamOnly = request.authorized?.constraints?.team_only;
|
||||
|
||||
return Process("models.user.Find", userId);
|
||||
}
|
||||
```
|
||||
|
||||
### Data Binding (`.json`)
|
||||
|
||||
```json
|
||||
{
|
||||
"userId": "$auth.user_id",
|
||||
"teamId": "$auth.team_id"
|
||||
}
|
||||
```
|
||||
|
||||
### HTML Templates
|
||||
|
||||
```html
|
||||
<p>Welcome, User {{ $auth.user_id }}</p>
|
||||
<p s:if="$auth.team_id">Team: {{ $auth.team_id }}</p>
|
||||
```
|
||||
|
||||
## Custom Guards
|
||||
|
||||
You can use custom process-based guards:
|
||||
|
||||
```json
|
||||
{
|
||||
"guard": "scripts.guards.CheckAdmin",
|
||||
"api": {
|
||||
"defaultGuard": "scripts.guards.CheckPermission"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The guard process receives the request context and should throw an exception to deny access.
|
||||
Loading…
Add table
Reference in a new issue