Skip to content
Open

scope #1011

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
14 changes: 14 additions & 0 deletions internal/auth/domain.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ type UserInfo struct {
ID model.UserID
GroupID user.GroupID
Permission Permission
Scope Scope
Legacy bool
}

// Auth is the basic authorization represent a user.
Expand All @@ -48,6 +50,18 @@ type Auth struct {
ID model.UserID // user id
GroupID user.GroupID
Permission Permission
Scope Scope
Legacy bool
}

type Scope map[string]bool

func (u Auth) HasScope(s string) bool {
if u.Legacy || u.Scope == nil {
return true
}

return u.Scope[s]
}

const nsfwThreshold = gtime.OneDay * 60
Expand Down
28 changes: 28 additions & 0 deletions internal/auth/domain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,31 @@ func TestNotAllowNsfw(t *testing.T) {

require.False(t, u.AllowNSFW())
}

func TestAuthHasScope(t *testing.T) {
t.Parallel()

u := auth.Auth{
Scope: auth.Scope{
"write:collection": true,
},
}

require.True(t, u.HasScope("write:collection"))
require.False(t, u.HasScope("write:indices"))
}

func TestAuthHasScopeLegacy(t *testing.T) {
t.Parallel()

u := auth.Auth{Legacy: true}
require.True(t, u.HasScope("write:collection"))
require.True(t, u.HasScope("any:scope"))
}

func TestAuthHasScopeNilScopeCompatible(t *testing.T) {
t.Parallel()

u := auth.Auth{}
require.True(t, u.HasScope("write:collection"))
}
23 changes: 21 additions & 2 deletions internal/auth/mysql_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package auth
import (
"context"
"database/sql"
"encoding/json"
"errors"
"time"

Expand Down Expand Up @@ -47,10 +48,11 @@ type mysqlRepo struct {

func (m mysqlRepo) GetByToken(ctx context.Context, token string) (UserInfo, error) {
var access struct {
UserID string `db:"user_id"`
UserID string `db:"user_id"`
Scope sql.NullString `db:"scope"`
}
err := m.db.GetContext(ctx, &access,
`select user_id from chii_oauth_access_tokens
`select user_id, scope from chii_oauth_access_tokens
where access_token = BINARY ? and expires > ? limit 1`, token, time.Now())
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
Expand Down Expand Up @@ -87,14 +89,31 @@ func (m mysqlRepo) GetByToken(ctx context.Context, token string) (UserInfo, erro
return UserInfo{}, errgo.Wrap(err, "parsing permission")
}

scope, legacy := parseTokenScope(access.Scope)

return UserInfo{
RegTime: time.Unix(u.Regdate, 0),
ID: id,
GroupID: u.GroupID,
Permission: perm,
Scope: scope,
Legacy: legacy,
}, nil
}

func parseTokenScope(scope sql.NullString) (Scope, bool) {
if !scope.Valid || scope.String == "" {
return nil, true
}

var parsed map[string]bool
if err := json.Unmarshal([]byte(scope.String), &parsed); err != nil {
return Scope{}, false
}

return parsed, false
}

func (m mysqlRepo) GetPermission(ctx context.Context, groupID uint8) (Permission, error) {
r, err := m.q.UserGroup.WithContext(ctx).Where(m.q.UserGroup.ID.Eq(groupID)).Take()
if err != nil {
Expand Down
42 changes: 42 additions & 0 deletions internal/auth/mysql_repository_scope_internal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// SPDX-License-Identifier: AGPL-3.0-only

package auth

import (
"database/sql"
"testing"

"github.com/stretchr/testify/require"
)

func TestParseTokenScope_LegacyNull(t *testing.T) {
t.Parallel()

scope, legacy := parseTokenScope(sql.NullString{})
require.True(t, legacy)
require.Nil(t, scope)
}

func TestParseTokenScope_LegacyEmptyString(t *testing.T) {
t.Parallel()

scope, legacy := parseTokenScope(sql.NullString{Valid: true, String: ""})
require.True(t, legacy)
require.Nil(t, scope)
}

func TestParseTokenScope_Object(t *testing.T) {
t.Parallel()

scope, legacy := parseTokenScope(sql.NullString{Valid: true, String: `{"write:collection":true,"write:indices":false}`})

Check failure on line 31 in internal/auth/mysql_repository_scope_internal_test.go

View workflow job for this annotation

GitHub Actions / lint

The line is 122 characters long, which exceeds the maximum of 120 characters. (lll)
require.False(t, legacy)
require.Equal(t, Scope{"write:collection": true, "write:indices": false}, scope)
}

func TestParseTokenScope_NonObject(t *testing.T) {
t.Parallel()

scope, legacy := parseTokenScope(sql.NullString{Valid: true, String: `["write:collection"]`})
require.False(t, legacy)
require.Empty(t, scope)
}
2 changes: 2 additions & 0 deletions internal/auth/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ func (s service) GetByToken(ctx context.Context, token string) (Auth, error) {
ID: a.ID,
GroupID: a.GroupID,
Permission: permission.Merge(a.Permission),
Scope: a.Scope,
Legacy: a.Legacy,
}, nil
}

Expand Down
63 changes: 43 additions & 20 deletions openapi/v0.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -742,13 +742,15 @@ paths:
schema:
"$ref": "#/components/schemas/ErrorDetail"
security:
- HTTPBearer: []
- HTTPBearer:
- write:collection
delete:
tags:
- 角色
summary: Uncollect character for current user
operationId: uncollectCharacterByCharacterIdAndUserId
description: 为当前用户取消收藏角色
description: |
为当前用户取消收藏角色
parameters:
- $ref: "#/components/parameters/path_character_id"
responses:
Expand All @@ -773,7 +775,8 @@ paths:
schema:
"$ref": "#/components/schemas/ErrorDetail"
security:
- HTTPBearer: []
- HTTPBearer:
- write:collection

"/v0/persons/{person_id}":
get:
Expand Down Expand Up @@ -931,7 +934,8 @@ paths:
schema:
"$ref": "#/components/schemas/ErrorDetail"
security:
- OptionalHTTPBearer: []
- HTTPBearer:
- write:collection
delete:
tags:
- 人物
Expand Down Expand Up @@ -962,7 +966,8 @@ paths:
schema:
"$ref": "#/components/schemas/ErrorDetail"
security:
- OptionalHTTPBearer: []
- HTTPBearer:
- write:collection

"/v0/users/{username}":
get:
Expand Down Expand Up @@ -1198,7 +1203,8 @@ paths:
schema:
"$ref": "#/components/schemas/ErrorDetail"
security:
- OptionalHTTPBearer: []
- HTTPBearer:
- write:collection
patch:
tags:
- 收藏
Expand Down Expand Up @@ -1239,7 +1245,8 @@ paths:
schema:
"$ref": "#/components/schemas/ErrorDetail"
security:
- OptionalHTTPBearer: []
- HTTPBearer:
- write:collection

"/v0/users/-/collections/{subject_id}/episodes":
get:
Expand Down Expand Up @@ -1348,7 +1355,8 @@ paths:
schema:
"$ref": "#/components/schemas/ErrorDetail"
security:
- HTTPBearer: []
- HTTPBearer:
- write:collection

"/v0/users/-/collections/-/episodes/{episode_id}":
get:
Expand Down Expand Up @@ -1424,7 +1432,8 @@ paths:
schema:
"$ref": "#/components/schemas/ErrorDetail"
security:
- HTTPBearer: []
- HTTPBearer:
- write:collection

"/v0/users/{username}/collections/-/characters":
get:
Expand Down Expand Up @@ -1782,7 +1791,8 @@ paths:
schema:
"$ref": "#/components/schemas/ErrorDetail"
security:
- HTTPBearer: []
- HTTPBearer:
- write:indices
"/v0/indices/{index_id}":
get:
tags:
Expand Down Expand Up @@ -1828,7 +1838,8 @@ paths:
"404":
"$ref": "#/components/responses/404"
security:
- HTTPBearer: []
- HTTPBearer:
- write:indices
"/v0/indices/{index_id}/subjects":
get:
tags:
Expand Down Expand Up @@ -1876,7 +1887,8 @@ paths:
"404":
"$ref": "#/components/responses/404"
security:
- HTTPBearer: []
- HTTPBearer:
- write:indices
"/v0/indices/{index_id}/subjects/{subject_id}":
put:
tags:
Expand All @@ -1902,7 +1914,8 @@ paths:
"400":
"$ref": "#/components/responses/400"
security:
- HTTPBearer: []
- HTTPBearer:
- write:indices
delete:
tags:
- 目录
Expand All @@ -1919,7 +1932,8 @@ paths:
"401":
"$ref": "#/components/responses/401"
security:
- HTTPBearer: []
- HTTPBearer:
- write:indices
"/v0/indices/{index_id}/collect":
post:
tags:
Expand All @@ -1939,13 +1953,15 @@ paths:
"500":
"$ref": "#/components/responses/500"
security:
- HTTPBearer: []
- HTTPBearer:
- write:collection
delete:
tags:
- 目录
summary: Uncollect index for current user
operationId: uncollectIndexByIndexIdAndUserId
description: 为当前用户取消收藏一条目录
description: |
为当前用户取消收藏一条目录
parameters:
- $ref: "#/components/parameters/path_index_id"
responses:
Expand All @@ -1958,7 +1974,8 @@ paths:
"500":
"$ref": "#/components/responses/500"
security:
- HTTPBearer: []
- HTTPBearer:
- write:collection
components:
parameters:
path_subject_id:
Expand Down Expand Up @@ -3316,9 +3333,15 @@ components:
description: 不强制要求用户认证,但是可能看不到某些敏感内容内容(如 NSFW 或者仅用户自己可见的收藏)
scheme: Bearer
HTTPBearer:
type: http
description: 需要使用 access token 进行认证
scheme: Bearer
type: oauth2
description: OAuth2 access token(写操作会校验 scope)
flows:
authorizationCode:
authorizationUrl: /oauth/authorize
tokenUrl: /oauth/access_token
scopes:
write:collection: 修改收藏相关数据
write:indices: 修改目录及目录条目
responses:
200-no-content:
description: Successful Response
Expand Down
23 changes: 23 additions & 0 deletions web/mw/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ import (
)

var errNeedLogin = res.Unauthorized("this API need authorization")
var errInsufficientScope = res.Forbidden("insufficient token scope")

const (
ScopeWriteCollection = "write:collection"
ScopeWriteIndices = "write:indices"
)

func NeedLogin(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
Expand All @@ -32,3 +38,20 @@ func NeedLogin(next echo.HandlerFunc) echo.HandlerFunc {
return next(c)
}
}

func NeedScope(scope string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
u := accessor.GetFromCtx(c)
if !u.Login {
return errNeedLogin
}

if !u.HasScope(scope) {
return errInsufficientScope
}

return next(c)
}
}
}
Loading
Loading