Files
cloudreve/middleware/auth.go

274 lines
7.4 KiB
Go
Raw Permalink Normal View History

2019-11-12 15:34:54 +08:00
package middleware
import (
"net/http"
"github.com/cloudreve/Cloudreve/v4/application/dependency"
"github.com/cloudreve/Cloudreve/v4/ent"
"github.com/cloudreve/Cloudreve/v4/inventory"
"github.com/cloudreve/Cloudreve/v4/inventory/types"
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver/oss"
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager"
"github.com/cloudreve/Cloudreve/v4/pkg/logging"
"github.com/cloudreve/Cloudreve/v4/pkg/request"
"github.com/cloudreve/Cloudreve/v4/pkg/util"
"github.com/cloudreve/Cloudreve/v4/pkg/auth"
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
2019-11-12 15:34:54 +08:00
"github.com/gin-gonic/gin"
)
const (
CallbackFailedStatusCode = http.StatusUnauthorized
2019-11-12 15:34:54 +08:00
)
2019-12-10 17:10:34 +08:00
// SignRequired 验证请求签名
Feat: aria2 download and transfer in slave node (#1040) * Feat: retrieve nodes from data table * Feat: master node ping slave node in REST API * Feat: master send scheduled ping request * Feat: inactive nodes recover loop * Modify: remove database operations from aria2 RPC caller implementation * Feat: init aria2 client in master node * Feat: Round Robin load balancer * Feat: create and monitor aria2 task in master node * Feat: salve receive and handle heartbeat * Fix: Node ID will be 0 in download record generated in older version * Feat: sign request headers with all `X-` prefix * Feat: API call to slave node will carry meta data in headers * Feat: call slave aria2 rpc method from master * Feat: get slave aria2 task status Feat: encode slave response data using gob * Feat: aria2 callback to master node / cancel or select task to slave node * Fix: use dummy aria2 client when caller initialize failed in master node * Feat: slave aria2 status event callback / salve RPC auth * Feat: prototype for slave driven filesystem * Feat: retry for init aria2 client in master node * Feat: init request client with global options * Feat: slave receive async task from master * Fix: competition write in request header * Refactor: dependency initialize order * Feat: generic message queue implementation * Feat: message queue implementation * Feat: master waiting slave transfer result * Feat: slave transfer file in stateless policy * Feat: slave transfer file in slave policy * Feat: slave transfer file in local policy * Feat: slave transfer file in OneDrive policy * Fix: failed to initialize update checker http client * Feat: list slave nodes for dashboard * Feat: test aria2 rpc connection in slave * Feat: add and save node * Feat: add and delete node in node pool * Fix: temp file cannot be removed when aria2 task fails * Fix: delete node in admin panel * Feat: edit node and get node info * Modify: delete unused settings
2021-10-31 09:41:56 +08:00
func SignRequired(authInstance auth.Auth) gin.HandlerFunc {
2019-12-10 17:10:34 +08:00
return func(c *gin.Context) {
var err error
switch c.Request.Method {
case http.MethodPut, http.MethodPost, http.MethodPatch:
err = auth.CheckRequest(c, authInstance, c.Request)
default:
err = auth.CheckURI(c, authInstance, c.Request.URL)
}
2019-12-10 20:17:21 +08:00
if err != nil {
c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeCredentialInvalid, err.Error(), err))
2019-12-10 20:17:21 +08:00
c.Abort()
return
2019-12-10 20:17:21 +08:00
}
2019-12-10 17:10:34 +08:00
c.Next()
}
}
2019-11-12 15:34:54 +08:00
// CurrentUser 获取登录用户
func CurrentUser() gin.HandlerFunc {
return func(c *gin.Context) {
dep := dependency.FromContext(c)
shouldContinue, err := dep.TokenAuth().VerifyAndRetrieveUser(c)
if err != nil {
c.JSON(200, serializer.Err(c, err))
c.Abort()
return
}
if shouldContinue {
// TODO: Logto handler
2019-11-12 15:34:54 +08:00
}
uid := inventory.UserIDFromContext(c)
if err := SetUserCtx(c, uid); err != nil {
c.JSON(200, serializer.Err(c, err))
c.Abort()
return
}
2019-11-12 15:34:54 +08:00
c.Next()
}
}
// SetUserCtx set the current login user via uid
func SetUserCtx(c *gin.Context, uid int) error {
dep := dependency.FromContext(c)
userClient := dep.UserClient()
loginUser, err := userClient.GetLoginUserByID(c, uid)
if err != nil {
return serializer.NewError(serializer.CodeDBError, "failed to get login user", err)
}
SetUserCtxByUser(c, loginUser)
return nil
}
func SetUserCtxByUser(c *gin.Context, user *ent.User) {
util.WithValue(c, inventory.UserCtx{}, user)
}
// LoginRequired 需要登录
func LoginRequired() gin.HandlerFunc {
2019-11-12 15:34:54 +08:00
return func(c *gin.Context) {
if u := inventory.UserFromContext(c); u != nil && !inventory.IsAnonymousUser(u) {
c.Next()
return
2019-11-12 15:34:54 +08:00
}
c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeCheckLogin, "Login required", nil))
2019-11-12 15:34:54 +08:00
c.Abort()
}
}
// WebDAVAuth 验证WebDAV登录及权限
func WebDAVAuth() gin.HandlerFunc {
return func(c *gin.Context) {
username, password, ok := c.Request.BasicAuth()
if !ok {
// OPTIONS 请求不需要鉴权
if c.Request.Method == http.MethodOptions {
c.Next()
return
}
c.Writer.Header()["WWW-Authenticate"] = []string{`Basic realm="cloudreve"`}
c.Status(http.StatusUnauthorized)
c.Abort()
return
}
dep := dependency.FromContext(c)
l := dep.Logger()
userClient := dep.UserClient()
expectedUser, err := userClient.GetActiveByDavAccount(c, username, password)
if err != nil {
if username == "" {
if u, err := userClient.GetByEmail(c, username); err == nil {
// Try login with known user but incorrect password, record audit log
SetUserCtxByUser(c, u)
}
}
l.Debug("WebDAVAuth: failed to get user %q with provided credential: %s", username, err)
c.Status(http.StatusUnauthorized)
c.Abort()
return
}
// Validate dav account
accounts, err := expectedUser.Edges.DavAccountsOrErr()
if err != nil || len(accounts) == 0 {
l.Debug("WebDAVAuth: failed to get user dav accounts %q with provided credential: %s", username, err)
c.Status(http.StatusUnauthorized)
c.Abort()
return
}
// 用户组已启用WebDAV
group, err := expectedUser.Edges.GroupOrErr()
if err != nil {
l.Debug("WebDAVAuth: user group not found: %s", err)
c.Status(http.StatusInternalServerError)
c.Abort()
return
}
if !group.Permissions.Enabled(int(types.GroupPermissionWebDAV)) {
c.Status(http.StatusForbidden)
l.Debug("WebDAVAuth: user %q does not have WebDAV permission.", expectedUser.Email)
c.Abort()
return
}
// 检查是否只读
if expectedUser.Edges.DavAccounts[0].Options.Enabled(int(types.DavAccountReadOnly)) {
switch c.Request.Method {
case http.MethodDelete, http.MethodPut, "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK":
c.Status(http.StatusForbidden)
c.Abort()
return
}
}
SetUserCtxByUser(c, expectedUser)
c.Next()
}
}
// 对上传会话进行验证
func UseUploadSession(policyType types.PolicyType) gin.HandlerFunc {
return func(c *gin.Context) {
// 验证key并查找用户
err := uploadCallbackCheck(c, policyType)
if err != nil {
c.JSON(CallbackFailedStatusCode, serializer.Err(c, err))
c.Abort()
return
}
c.Next()
}
}
2020-01-15 10:14:15 +08:00
// uploadCallbackCheck 对上传回调请求的 callback key 进行验证,如果成功则返回上传用户
func uploadCallbackCheck(c *gin.Context, policyType types.PolicyType) error {
2020-01-15 10:14:15 +08:00
// 验证 Callback Key
sessionID := c.Param("sessionID")
if sessionID == "" {
return serializer.NewError(serializer.CodeParamErr, "Session ID cannot be empty", nil)
2020-01-15 10:14:15 +08:00
}
dep := dependency.FromContext(c)
callbackSessionRaw, exist := dep.KV().Get(manager.UploadSessionCachePrefix + sessionID)
2020-01-15 10:14:15 +08:00
if !exist {
return serializer.NewError(serializer.CodeUploadSessionExpired, "Upload session does not exist or expired", nil)
2020-01-15 10:14:15 +08:00
}
callbackSession := callbackSessionRaw.(fs.UploadSession)
c.Set(manager.UploadSessionCtx, &callbackSession)
if callbackSession.Policy.Type != string(policyType) {
return serializer.NewError(serializer.CodePolicyNotAllowed, "", nil)
}
2020-01-15 10:14:15 +08:00
if err := SetUserCtx(c, callbackSession.UID); err != nil {
return err
2020-01-15 10:14:15 +08:00
}
return nil
2020-01-15 10:14:15 +08:00
}
// RemoteCallbackAuth 远程回调签名验证
func RemoteCallbackAuth() gin.HandlerFunc {
return func(c *gin.Context) {
2020-01-15 10:14:15 +08:00
// 验证签名
session := c.MustGet(manager.UploadSessionCtx).(*fs.UploadSession)
if session.Policy.Edges.Node == nil {
c.JSON(CallbackFailedStatusCode, serializer.ErrWithDetails(c, serializer.CodeCredentialInvalid, "Node not found", nil))
c.Abort()
return
}
2022-03-23 20:05:10 +08:00
authInstance := auth.HMACAuth{SecretKey: []byte(session.Policy.Edges.Node.SlaveKey)}
if err := auth.CheckRequest(c, authInstance, c.Request); err != nil {
c.JSON(CallbackFailedStatusCode, serializer.ErrWithDetails(c, serializer.CodeCredentialInvalid, err.Error(), err))
c.Abort()
return
}
c.Next()
}
}
2020-01-16 13:36:13 +08:00
// OSSCallbackAuth 阿里云OSS回调签名验证
func OSSCallbackAuth() gin.HandlerFunc {
return func(c *gin.Context) {
dep := dependency.FromContext(c)
err := oss.VerifyCallbackSignature(c.Request, dep.KV(), dep.RequestClient(
request.WithContext(c),
request.WithLogger(logging.FromContext(c)),
))
if err != nil {
dep.Logger().Debug("Failed to verify callback request: %s", err)
c.JSON(401, serializer.GeneralUploadCallbackFailed{Error: "Failed to verify callback request."})
c.Abort()
return
}
2020-01-16 13:36:13 +08:00
c.Next()
}
}
2020-01-18 10:40:03 +08:00
2020-02-22 16:22:04 +08:00
// IsAdmin 必须为管理员用户组
func IsAdmin() gin.HandlerFunc {
return func(c *gin.Context) {
user := inventory.UserFromContext(c)
if !user.Edges.Group.Permissions.Enabled(int(types.GroupPermissionIsAdmin)) {
c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeNoPermissionErr, "", nil))
2020-02-22 16:22:04 +08:00
c.Abort()
return
}
c.Next()
}
}