mirror of
https://codeberg.org/shroff/phylum.git
synced 2026-01-04 18:50:42 -06:00
[server] Allow basic and bearer auth for api access
This commit is contained in:
@@ -12,7 +12,7 @@ func Setup(r *gin.RouterGroup, a *core.App) {
|
||||
routes.SetupAuthRoutes(r, a)
|
||||
|
||||
// Authenticated routes
|
||||
r.Use(auth.CreateBearerAuthHandler(a))
|
||||
r.Use(auth.CreateApiAuthHandler(a))
|
||||
routes.SetupResourceRoutes(r)
|
||||
routes.SetupUserRoutes(r)
|
||||
routes.SetupMobileRoutes(r)
|
||||
|
||||
81
server/internal/api/auth/auth_api.go
Normal file
81
server/internal/api/auth/auth_api.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/shroff/phylum/server/internal/api/errors"
|
||||
"github.com/shroff/phylum/server/internal/core"
|
||||
)
|
||||
|
||||
type authHeaderType = int
|
||||
|
||||
const (
|
||||
authHeaderNone = -1
|
||||
authHeaderUnknown = 0
|
||||
authHeaderBasic = 1
|
||||
authHeaderBearer = 2
|
||||
)
|
||||
|
||||
const errCodeAuthRequred = "auth_required"
|
||||
const errCodeCredentialsInvalid = "credentials_invalid"
|
||||
|
||||
func CreateApiAuthHandler(a *core.App) func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
if user, err := extractUserDetails(a, c); err != nil {
|
||||
panic(err)
|
||||
} else {
|
||||
c.Set(keyUser, user)
|
||||
c.Set(keyFileSystem, a.OpenFileSystem(ctx, user))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func extractUserDetails(a *core.App, c *gin.Context) (core.User, error) {
|
||||
if header := c.Request.Header.Get("Authorization"); header == "" {
|
||||
return core.User{}, errors.New(http.StatusUnauthorized, errCodeAuthRequred, "Authorization Required")
|
||||
} else if auth, ok := checkAuthHeader(header, "basic"); ok {
|
||||
if username, password, ok := decodeBasicAuth(auth); ok {
|
||||
if user, err := a.VerifyUserPassword(c.Request.Context(), username, password); err == nil {
|
||||
return user, nil
|
||||
} else if errors.Is(err, core.ErrCredentialsInvalid) {
|
||||
return core.User{}, errors.New(http.StatusUnauthorized, errCodeCredentialsInvalid, "Invalid Credentials")
|
||||
} else {
|
||||
return core.User{}, err
|
||||
}
|
||||
}
|
||||
} else if token, ok := checkAuthHeader(header, "bearer"); ok {
|
||||
if user, err := a.ReadAccessToken(c.Request.Context(), token); err == nil {
|
||||
return user, nil
|
||||
} else if errors.Is(err, core.ErrTokenExpired) || errors.Is(err, core.ErrTokenInvalid) {
|
||||
return core.User{}, errors.New(http.StatusUnauthorized, errCodeCredentialsInvalid, "Invalid Credentials")
|
||||
} else {
|
||||
return core.User{}, err
|
||||
}
|
||||
}
|
||||
return core.User{}, errors.New(http.StatusUnauthorized, errCodeAuthRequred, "Authorization scheme not recognized")
|
||||
}
|
||||
|
||||
func checkAuthHeader(header, prefix string) (string, bool) {
|
||||
prefix = prefix + " "
|
||||
if len(header) < len(prefix) || !strings.EqualFold(header[:len(prefix)], prefix) {
|
||||
return "", false
|
||||
}
|
||||
return header[len(prefix):], true
|
||||
}
|
||||
|
||||
func decodeBasicAuth(auth string) (username, password string, ok bool) {
|
||||
c, err := base64.StdEncoding.DecodeString(auth)
|
||||
if err != nil {
|
||||
return "", "", false
|
||||
}
|
||||
cs := string(c)
|
||||
username, password, ok = strings.Cut(cs, ":")
|
||||
if !ok {
|
||||
return "", "", false
|
||||
}
|
||||
return username, password, true
|
||||
}
|
||||
@@ -1,33 +1,28 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/shroff/phylum/server/internal/api/errors"
|
||||
"github.com/shroff/phylum/server/internal/core"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func CreateBasicAuthHandler(a *core.App) func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
if username, pass, ok := c.Request.BasicAuth(); !ok {
|
||||
c.Header("WWW-Authenticate", "Basic realm=\"Phylum WebDAV\"")
|
||||
c.AbortWithStatus(http.StatusUnauthorized)
|
||||
} else if user, err := a.VerifyUserPassword(c.Request.Context(), username, pass); err != nil {
|
||||
if errors.Is(err, core.ErrCredentialsInvalid) {
|
||||
c.Header("WWW-Authenticate", "Basic realm=\"Phylum WebDAV\"")
|
||||
c.AbortWithStatus(http.StatusUnauthorized)
|
||||
} else {
|
||||
logrus.Warn(err)
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
authSuccess := false
|
||||
if username, pass, ok := c.Request.BasicAuth(); ok {
|
||||
if user, err := a.VerifyUserPassword(c.Request.Context(), username, pass); err == nil {
|
||||
c.Set(keyUser, user)
|
||||
c.Set(keyFileSystem, a.OpenFileSystem(c.Request.Context(), user))
|
||||
authSuccess = true
|
||||
} else if !errors.Is(err, core.ErrCredentialsInvalid) {
|
||||
panic(err)
|
||||
}
|
||||
} else if fs, err := a.OpenFileSystem(c.Request.Context(), user); err != nil {
|
||||
logrus.Warn(err)
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
} else {
|
||||
c.Set(keyUser, user)
|
||||
c.Set(keyFileSystem, fs)
|
||||
}
|
||||
if !authSuccess {
|
||||
c.Header("WWW-Authenticate", "Basic realm=\"Phylum WebDAV\"")
|
||||
panic(errors.New(http.StatusUnauthorized, errCodeAuthRequred, "Authorization Required"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/shroff/phylum/server/internal/api/errors"
|
||||
"github.com/shroff/phylum/server/internal/core"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const errCodeAuthRequred = "auth_required"
|
||||
const errCodeTokenInvalid = "token_invalid"
|
||||
|
||||
func CreateBearerAuthHandler(a *core.App) func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
panic(errors.New(401, errCodeAuthRequred, "Authorization Header Required"))
|
||||
}
|
||||
authParts := strings.Split(authHeader, " ")
|
||||
if len(authParts) != 2 {
|
||||
panic(errors.New(401, errCodeAuthRequred, "Authorization Header Malformed"))
|
||||
}
|
||||
if authParts[0] != "bearer" {
|
||||
panic(errors.New(401, errCodeAuthRequred, "Authorization Type Not Recognized"))
|
||||
}
|
||||
|
||||
user, err := a.ReadAccessToken(ctx, authParts[1])
|
||||
if err != nil {
|
||||
if errors.Is(err, core.ErrTokenExpired) || errors.Is(err, core.ErrTokenInvalid) {
|
||||
panic(errors.New(401, errCodeTokenInvalid, "Authorization Token Invalid"))
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
if fs, err := a.OpenFileSystem(ctx, user); err != nil {
|
||||
logrus.Warn(err)
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
} else {
|
||||
c.Set(keyUser, user)
|
||||
c.Set(keyFileSystem, fs)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -263,23 +263,15 @@ func setupResourceChpermCommand() *cobra.Command {
|
||||
}
|
||||
|
||||
func openFileSystemFromFlags(cmd *cobra.Command) {
|
||||
var user core.User
|
||||
if value, err := cmd.Flags().GetString("user"); err != nil {
|
||||
logrus.Fatal(err)
|
||||
} else {
|
||||
if u, err := core.Default.UserByEmail(context.Background(), value); err != nil {
|
||||
if user, err := core.Default.UserByEmail(context.Background(), value); err != nil {
|
||||
logrus.Fatal(err)
|
||||
} else {
|
||||
user = u
|
||||
fs = core.Default.OpenFileSystem(context.Background(), user)
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
fs, err = core.Default.OpenFileSystem(context.Background(), user)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func setupUserFlags(cmd *cobra.Command) {
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/shroff/phylum/server/internal/storage"
|
||||
)
|
||||
|
||||
const defaultUserEmail = "admin@phylum"
|
||||
const defaultUserEmail = "phylum"
|
||||
|
||||
type App struct {
|
||||
Debug bool
|
||||
@@ -38,11 +38,7 @@ func (a *App) setupAppData(ctx context.Context) error {
|
||||
u, err = a.populateData(ctx)
|
||||
}
|
||||
if err == nil {
|
||||
if fs, err := OpenFileSystem(a.db, ctx, a.cs, u.ID, u.Root); err != nil {
|
||||
return err
|
||||
} else {
|
||||
a.Rootfs = fs
|
||||
}
|
||||
a.Rootfs = OpenFileSystem(a.db, ctx, a.cs, u.ID, u.Root)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -104,6 +100,6 @@ func (a *App) populateData(ctx context.Context) (user db.User, e error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (a App) OpenFileSystem(ctx context.Context, user User) (FileSystem, error) {
|
||||
func (a App) OpenFileSystem(ctx context.Context, user User) FileSystem {
|
||||
return OpenFileSystem(a.db, ctx, a.cs, user.ID, user.Root)
|
||||
}
|
||||
|
||||
@@ -55,14 +55,14 @@ type filesystem struct {
|
||||
user int32
|
||||
}
|
||||
|
||||
func OpenFileSystem(dbh *db.DbHandler, ctx context.Context, cs storage.Storage, user int32, rootID uuid.UUID) (FileSystem, error) {
|
||||
func OpenFileSystem(dbh *db.DbHandler, ctx context.Context, cs storage.Storage, user int32, rootID uuid.UUID) FileSystem {
|
||||
return filesystem{
|
||||
db: dbh,
|
||||
ctx: ctx,
|
||||
cs: cs,
|
||||
rootID: rootID,
|
||||
user: user,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (f filesystem) RootID() uuid.UUID {
|
||||
|
||||
@@ -188,7 +188,7 @@ func (q *Queries) ResourceByPath(ctx context.Context, arg ResourceByPathParams)
|
||||
}
|
||||
|
||||
const updatePermissionsForResource = `-- name: UpdatePermissionsForResource :exec
|
||||
UPDATE resources SET permissions[$1::integer] = $2::int WHERE id = $3::uuid
|
||||
UPDATE resources SET permissions[$1::int] = to_json($2::int) WHERE id = $3::uuid
|
||||
`
|
||||
|
||||
type UpdatePermissionsForResourceParams struct {
|
||||
|
||||
@@ -53,28 +53,4 @@ ON r.id = n.resid
|
||||
WHERE n.parent IS NULL;
|
||||
|
||||
-- name: UpdatePermissionsForResource :exec
|
||||
UPDATE resources SET permissions[@user_id::integer] = @permission::int WHERE id = @resource_id::uuid;
|
||||
|
||||
-- -- name: GetLocalPermissionsForResource :many
|
||||
-- SELECT p.user_id, CAST(p.permission AS INT) AS permission
|
||||
-- FROM permissions p
|
||||
-- WHERE p.resource_id = @resource_id::uuid;
|
||||
|
||||
|
||||
-- -- name: GetInheritedPermissionsForResource :many
|
||||
-- WITH RECURSIVE nodes(id, parent) AS (
|
||||
-- SELECT r.id, r.parent
|
||||
-- FROM resources r
|
||||
-- WHERE r.id = @resource_id::uuid
|
||||
-- UNION ALL (
|
||||
-- SELECT r.id, r.parent
|
||||
-- FROM resources r
|
||||
-- JOIN nodes n
|
||||
-- ON r.id = n.parent
|
||||
-- )
|
||||
-- )
|
||||
-- SELECT p.user_id, CAST(MAX(p.permission) AS INT) AS permission
|
||||
-- FROM permissions p
|
||||
-- JOIN nodes n
|
||||
-- ON n.id = p.resource_id
|
||||
-- GROUP BY p.user_id;
|
||||
UPDATE resources SET permissions[@user_id::int] = to_json(@permission::int) WHERE id = @resource_id::uuid;
|
||||
Reference in New Issue
Block a user