mirror of
https://github.com/datarhei/core.git
synced 2025-10-06 00:17:07 +08:00
Define default policies to mimic current behaviour
This commit is contained in:
@@ -400,7 +400,10 @@ func (a *api) start() error {
|
||||
Enable: cfg.Storage.Memory.Auth.Enable,
|
||||
Password: cfg.Storage.Memory.Auth.Password,
|
||||
},
|
||||
Token: cfg.RTMP.Token,
|
||||
Token: []string{
|
||||
cfg.RTMP.Token,
|
||||
cfg.SRT.Token,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -426,17 +429,42 @@ func (a *api) start() error {
|
||||
secret = cfg.API.Auth.Username + cfg.API.Auth.Password + cfg.API.Auth.JWT.Secret
|
||||
}
|
||||
|
||||
fmt.Printf("superuser: %+v\n", superuser)
|
||||
|
||||
iam, err := iam.NewIAM(iam.Config{
|
||||
FS: fs,
|
||||
Superuser: superuser,
|
||||
JWTRealm: "datarhei-core",
|
||||
JWTSecret: secret,
|
||||
Logger: a.log.logger.core.WithComponent("IAM"),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("iam: %w", err)
|
||||
}
|
||||
|
||||
iam.RemovePolicy("$anon", "$none", "", "")
|
||||
iam.RemovePolicy("$localhost", "$none", "", "")
|
||||
|
||||
iam.AddPolicy("$anon", "$none", "fs:/**", "GET|HEAD|OPTIONS")
|
||||
iam.AddPolicy("$anon", "$none", "api:/api", "GET|HEAD|OPTIONS")
|
||||
iam.AddPolicy("$anon", "$none", "api:/api/v3/widget/process/**", "GET|HEAD|OPTIONS")
|
||||
|
||||
iam.AddPolicy("$localhost", "$none", "fs:/**", "GET|HEAD|OPTIONS")
|
||||
iam.AddPolicy("$localhost", "$none", "api:/api", "GET|HEAD|OPTIONS")
|
||||
iam.AddPolicy("$localhost", "$none", "api:/api/v3/widget/process/**", "GET|HEAD|OPTIONS")
|
||||
|
||||
if !cfg.API.Auth.Enable {
|
||||
iam.AddPolicy("$anon", "$none", "api:/api/**", "GET|HEAD|OPTIONS|POST|PUT|DELETE")
|
||||
iam.AddPolicy("$localhost", "$none", "api:/api/**", "GET|HEAD|OPTIONS|POST|PUT|DELETE")
|
||||
}
|
||||
|
||||
if cfg.API.Auth.DisableLocalhost {
|
||||
iam.AddPolicy("$localhost", "$none", "api:/api/**", "GET|HEAD|OPTIONS|POST|PUT|DELETE")
|
||||
}
|
||||
|
||||
if !cfg.Storage.Memory.Auth.Enable {
|
||||
iam.AddPolicy("$anon", "$none", "fs:/memfs/**", "GET|HEAD|OPTIONS|POST|PUT|DELETE")
|
||||
iam.AddPolicy("$localhost", "$none", "fs:/memfs/**", "GET|HEAD|OPTIONS|POST|PUT|DELETE")
|
||||
}
|
||||
|
||||
a.iam = iam
|
||||
}
|
||||
|
||||
|
@@ -84,7 +84,7 @@ func (h *FSHandler) PutFile(c echo.Context) error {
|
||||
|
||||
_, created, err := h.fs.Filesystem.WriteFileReader(path, req.Body)
|
||||
if err != nil {
|
||||
return api.Err(http.StatusBadRequest, "%s", err)
|
||||
return api.Err(http.StatusBadRequest, "Bad request", "%s", err)
|
||||
}
|
||||
|
||||
if h.fs.Cache != nil {
|
||||
|
346
http/jwt/jwt.go
346
http/jwt/jwt.go
@@ -1,346 +0,0 @@
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/datarhei/core/v16/app"
|
||||
"github.com/datarhei/core/v16/http/api"
|
||||
"github.com/datarhei/core/v16/http/handler/util"
|
||||
"github.com/datarhei/core/v16/iam"
|
||||
|
||||
jwtgo "github.com/golang-jwt/jwt/v4"
|
||||
"github.com/google/uuid"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
)
|
||||
|
||||
// The Config type holds information that is required to create a new JWT provider
|
||||
type Config struct {
|
||||
Realm string
|
||||
Secret string
|
||||
SkipLocalhost bool
|
||||
IAM iam.IAM
|
||||
}
|
||||
|
||||
// JWT provides access to a JWT provider
|
||||
type JWT interface {
|
||||
// Middleware returns an echo middleware
|
||||
Middleware() echo.MiddlewareFunc
|
||||
|
||||
// LoginHandler is an echo route handler for retrieving a JWT
|
||||
LoginHandler(c echo.Context) error
|
||||
|
||||
// RefreshHandle is an echo route handler for refreshing a JWT
|
||||
RefreshHandler(c echo.Context) error
|
||||
}
|
||||
|
||||
type jwt struct {
|
||||
realm string
|
||||
skipLocalhost bool
|
||||
secret []byte
|
||||
accessValidFor time.Duration
|
||||
refreshValidFor time.Duration
|
||||
config middleware.JWTConfig
|
||||
middleware echo.MiddlewareFunc
|
||||
iam iam.IAM
|
||||
}
|
||||
|
||||
// New returns a new JWT provider
|
||||
func New(config Config) (JWT, error) {
|
||||
j := &jwt{
|
||||
realm: config.Realm,
|
||||
skipLocalhost: config.SkipLocalhost,
|
||||
secret: []byte(config.Secret),
|
||||
accessValidFor: time.Minute * 10,
|
||||
refreshValidFor: time.Hour * 24,
|
||||
}
|
||||
|
||||
if len(j.secret) == 0 {
|
||||
return nil, fmt.Errorf("the JWT secret must not be empty")
|
||||
}
|
||||
|
||||
skipperFunc := func(c echo.Context) bool {
|
||||
if j.skipLocalhost {
|
||||
ip := c.RealIP()
|
||||
|
||||
if ip == "127.0.0.1" || ip == "::1" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
j.config = middleware.JWTConfig{
|
||||
Skipper: skipperFunc,
|
||||
SigningMethod: middleware.AlgorithmHS256,
|
||||
ContextKey: "user",
|
||||
TokenLookup: "header:" + echo.HeaderAuthorization,
|
||||
AuthScheme: "Bearer",
|
||||
Claims: jwtgo.MapClaims{},
|
||||
ErrorHandlerWithContext: j.ErrorHandler,
|
||||
SuccessHandler: func(c echo.Context) {
|
||||
token := c.Get("user").(*jwtgo.Token)
|
||||
|
||||
var subject string
|
||||
if claims, ok := token.Claims.(jwtgo.MapClaims); ok {
|
||||
if sub, ok := claims["sub"]; ok {
|
||||
subject = sub.(string)
|
||||
}
|
||||
}
|
||||
|
||||
c.Set("user", subject)
|
||||
|
||||
var usefor string
|
||||
if claims, ok := token.Claims.(jwtgo.MapClaims); ok {
|
||||
if sub, ok := claims["usefor"]; ok {
|
||||
usefor = sub.(string)
|
||||
}
|
||||
}
|
||||
|
||||
c.Set("usefor", usefor)
|
||||
},
|
||||
ParseTokenFunc: j.parseToken,
|
||||
}
|
||||
|
||||
return j, nil
|
||||
}
|
||||
|
||||
func (j *jwt) parseToken(auth string, c echo.Context) (interface{}, error) {
|
||||
keyFunc := func(*jwtgo.Token) (interface{}, error) { return j.secret, nil }
|
||||
|
||||
var token *jwtgo.Token
|
||||
var err error
|
||||
|
||||
token, err = jwtgo.Parse(auth, keyFunc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !token.Valid {
|
||||
return nil, errors.New("invalid token")
|
||||
}
|
||||
|
||||
if _, ok := token.Claims.(jwtgo.MapClaims)["sub"]; !ok {
|
||||
return nil, fmt.Errorf("sub claim is required")
|
||||
}
|
||||
|
||||
if _, ok := token.Claims.(jwtgo.MapClaims)["usefor"]; !ok {
|
||||
return nil, fmt.Errorf("usefor claim is required")
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func (j *jwt) ErrorHandler(err error, c echo.Context) error {
|
||||
if c.Request().URL.Path == "/api" {
|
||||
return c.JSON(http.StatusOK, api.MinimalAbout{
|
||||
App: app.Name,
|
||||
Auths: []string{},
|
||||
Version: api.VersionMinimal{
|
||||
Number: app.Version.MajorString(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return api.Err(http.StatusUnauthorized, "Missing or invalid JWT token")
|
||||
}
|
||||
|
||||
func (j *jwt) Middleware() echo.MiddlewareFunc {
|
||||
if j.middleware == nil {
|
||||
j.middleware = middleware.JWTWithConfig(j.config)
|
||||
}
|
||||
|
||||
return j.middleware
|
||||
}
|
||||
|
||||
// LoginHandler returns an access token and a refresh token
|
||||
// @Summary Retrieve an access and a refresh token
|
||||
// @Description Retrieve valid JWT access and refresh tokens to use for accessing the API. Login either by username/password or Auth0 token
|
||||
// @ID jwt-login
|
||||
// @Produce json
|
||||
// @Param data body api.Login true "Login data"
|
||||
// @Success 200 {object} api.JWT
|
||||
// @Failure 400 {object} api.Error
|
||||
// @Failure 403 {object} api.Error
|
||||
// @Failure 500 {object} api.Error
|
||||
// @Security Auth0KeyAuth
|
||||
// @Router /api/login [post]
|
||||
func (j *jwt) LoginHandler(c echo.Context) error {
|
||||
ok, subject, err := j.validateLogin(c)
|
||||
|
||||
if ok {
|
||||
if err != nil {
|
||||
time.Sleep(5 * time.Second)
|
||||
return api.Err(http.StatusUnauthorized, "Invalid authorization credentials", "%s", err)
|
||||
}
|
||||
} else {
|
||||
time.Sleep(5 * time.Second)
|
||||
return api.Err(http.StatusBadRequest, "Missing authorization credentials")
|
||||
}
|
||||
|
||||
at, rt, err := j.createToken(subject)
|
||||
if err != nil {
|
||||
return api.Err(http.StatusInternalServerError, "Failed to create JWT", "%s", err)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, api.JWT{
|
||||
AccessToken: at,
|
||||
RefreshToken: rt,
|
||||
})
|
||||
}
|
||||
|
||||
func (j *jwt) validateLogin(c echo.Context) (bool, string, error) {
|
||||
ok, subject, err := j.validateUserpassLogin(c)
|
||||
if ok {
|
||||
return ok, subject, err
|
||||
}
|
||||
|
||||
return j.validateAuth0Login(c)
|
||||
}
|
||||
|
||||
func (j *jwt) validateUserpassLogin(c echo.Context) (bool, string, error) {
|
||||
var login api.Login
|
||||
|
||||
if err := util.ShouldBindJSON(c, &login); err != nil {
|
||||
return false, "", nil
|
||||
}
|
||||
|
||||
identity, err := j.iam.GetIdentity(login.Username)
|
||||
if err != nil {
|
||||
return true, "", fmt.Errorf("invalid username or password")
|
||||
}
|
||||
|
||||
if !identity.VerifyAPIPassword(login.Password) {
|
||||
return true, "", fmt.Errorf("invalid username or password")
|
||||
}
|
||||
|
||||
return true, identity.Name(), nil
|
||||
}
|
||||
|
||||
func (j *jwt) validateAuth0Login(c echo.Context) (bool, string, error) {
|
||||
// Look for an Auth header
|
||||
values := c.Request().Header.Values("Authorization")
|
||||
prefix := "Bearer "
|
||||
|
||||
auth := ""
|
||||
for _, value := range values {
|
||||
if !strings.HasPrefix(value, prefix) {
|
||||
continue
|
||||
}
|
||||
|
||||
auth = value[len(prefix):]
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
if len(auth) == 0 {
|
||||
return false, "", nil
|
||||
}
|
||||
|
||||
p := &jwtgo.Parser{}
|
||||
token, _, err := p.ParseUnverified(auth, jwtgo.MapClaims{})
|
||||
if err != nil {
|
||||
return false, "", nil
|
||||
}
|
||||
|
||||
var subject string
|
||||
if claims, ok := token.Claims.(jwtgo.MapClaims); ok {
|
||||
if sub, ok := claims["sub"]; ok {
|
||||
subject = sub.(string)
|
||||
}
|
||||
}
|
||||
|
||||
identity, err := j.iam.GetIdentityByAuth0(subject)
|
||||
if err != nil {
|
||||
return true, "", fmt.Errorf("invalid token")
|
||||
}
|
||||
|
||||
if !identity.VerifyAPIAuth0(auth) {
|
||||
return true, "", fmt.Errorf("invalid token")
|
||||
}
|
||||
|
||||
return true, identity.Name(), nil
|
||||
}
|
||||
|
||||
// RefreshHandler returns a new refresh token
|
||||
// @Summary Retrieve a new access token
|
||||
// @Description Retrieve a new access token by providing the refresh token
|
||||
// @ID jwt-refresh
|
||||
// @Produce json
|
||||
// @Success 200 {object} api.JWTRefresh
|
||||
// @Failure 500 {object} api.Error
|
||||
// @Security ApiRefreshKeyAuth
|
||||
// @Router /api/login/refresh [get]
|
||||
func (j *jwt) RefreshHandler(c echo.Context) error {
|
||||
subject, ok := c.Get("user").(string)
|
||||
if !ok {
|
||||
return api.Err(http.StatusForbidden, "Invalid token")
|
||||
}
|
||||
|
||||
usefor, ok := c.Get("usefor").(string)
|
||||
if !ok {
|
||||
return api.Err(http.StatusForbidden, "Invalid token")
|
||||
}
|
||||
|
||||
if usefor != "refresh" {
|
||||
return api.Err(http.StatusForbidden, "Invalid token")
|
||||
}
|
||||
|
||||
at, _, err := j.createToken(subject)
|
||||
if err != nil {
|
||||
return api.Err(http.StatusInternalServerError, "Failed to create JWT", "%s", err)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, api.JWTRefresh{
|
||||
AccessToken: at,
|
||||
})
|
||||
}
|
||||
|
||||
// Already assigned claims: https://www.iana.org/assignments/jwt/jwt.xhtml
|
||||
|
||||
func (j *jwt) createToken(username string) (string, string, error) {
|
||||
now := time.Now()
|
||||
accessExpires := now.Add(j.accessValidFor)
|
||||
refreshExpires := now.Add(j.refreshValidFor)
|
||||
|
||||
// Create access token
|
||||
accessToken := jwtgo.NewWithClaims(jwtgo.SigningMethodHS256, jwtgo.MapClaims{
|
||||
"iss": j.realm,
|
||||
"sub": username,
|
||||
"usefor": "access",
|
||||
"iat": now.Unix(),
|
||||
"exp": accessExpires.Unix(),
|
||||
"exi": uint64(accessExpires.Sub(now).Seconds()),
|
||||
"jti": uuid.New().String(),
|
||||
})
|
||||
|
||||
// Generate encoded access token
|
||||
at, err := accessToken.SignedString(j.secret)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// Create refresh token
|
||||
refreshToken := jwtgo.NewWithClaims(jwtgo.SigningMethodHS256, jwtgo.MapClaims{
|
||||
"iss": j.realm,
|
||||
"sub": username,
|
||||
"usefor": "refresh",
|
||||
"iat": now.Unix(),
|
||||
"exp": refreshExpires.Unix(),
|
||||
"exi": uint64(refreshExpires.Sub(now).Seconds()),
|
||||
"jti": uuid.New().String(),
|
||||
})
|
||||
|
||||
// Generate encoded refresh token
|
||||
rt, err := refreshToken.SignedString(j.secret)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return at, rt, nil
|
||||
}
|
@@ -1,124 +0,0 @@
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/datarhei/core/v16/http/api"
|
||||
"github.com/datarhei/core/v16/http/handler/util"
|
||||
"github.com/datarhei/core/v16/iam"
|
||||
|
||||
jwtgo "github.com/golang-jwt/jwt/v4"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
type Validator interface {
|
||||
String() string
|
||||
|
||||
// Validate returns true if it identified itself as validator for
|
||||
// that request. False if it doesn't handle this request. The string
|
||||
// is the username. An error is only returned if it identified itself
|
||||
// as validator but there was an error during validation.
|
||||
Validate(c echo.Context) (bool, string, error)
|
||||
Cancel()
|
||||
}
|
||||
|
||||
type localValidator struct {
|
||||
iam iam.IAM
|
||||
}
|
||||
|
||||
func NewLocalValidator(iam iam.IAM) (Validator, error) {
|
||||
v := &localValidator{
|
||||
iam: iam,
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (v *localValidator) String() string {
|
||||
return "localjwt"
|
||||
}
|
||||
|
||||
func (v *localValidator) Validate(c echo.Context) (bool, string, error) {
|
||||
var login api.Login
|
||||
|
||||
if err := util.ShouldBindJSON(c, &login); err != nil {
|
||||
return false, "", nil
|
||||
}
|
||||
|
||||
identity, err := v.iam.GetIdentity(login.Username)
|
||||
if err != nil {
|
||||
return true, "", fmt.Errorf("invalid username or password")
|
||||
}
|
||||
|
||||
if !identity.VerifyAPIPassword(login.Password) {
|
||||
return true, "", fmt.Errorf("invalid username or password")
|
||||
}
|
||||
|
||||
return true, identity.Name(), nil
|
||||
}
|
||||
|
||||
func (v *localValidator) Cancel() {}
|
||||
|
||||
type auth0Validator struct {
|
||||
iam iam.IAM
|
||||
}
|
||||
|
||||
func NewAuth0Validator(iam iam.IAM) (Validator, error) {
|
||||
v := &auth0Validator{
|
||||
iam: iam,
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (v auth0Validator) String() string {
|
||||
return fmt.Sprintf("auth0 domain=%s audience=%s clientid=%s", "", "", "")
|
||||
}
|
||||
|
||||
func (v *auth0Validator) Validate(c echo.Context) (bool, string, error) {
|
||||
// Look for an Auth header
|
||||
values := c.Request().Header.Values("Authorization")
|
||||
prefix := "Bearer "
|
||||
|
||||
auth := ""
|
||||
for _, value := range values {
|
||||
if !strings.HasPrefix(value, prefix) {
|
||||
continue
|
||||
}
|
||||
|
||||
auth = value[len(prefix):]
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
if len(auth) == 0 {
|
||||
return false, "", nil
|
||||
}
|
||||
|
||||
p := &jwtgo.Parser{}
|
||||
token, _, err := p.ParseUnverified(auth, jwtgo.MapClaims{})
|
||||
if err != nil {
|
||||
return false, "", nil
|
||||
}
|
||||
|
||||
var subject string
|
||||
if claims, ok := token.Claims.(jwtgo.MapClaims); ok {
|
||||
if sub, ok := claims["sub"]; ok {
|
||||
subject = sub.(string)
|
||||
}
|
||||
}
|
||||
|
||||
identity, err := v.iam.GetIdentityByAuth0(subject)
|
||||
if err != nil {
|
||||
return true, "", fmt.Errorf("invalid token")
|
||||
}
|
||||
|
||||
if !identity.VerifyAPIAuth0(auth) {
|
||||
return true, "", fmt.Errorf("invalid token")
|
||||
}
|
||||
|
||||
return true, identity.Name(), nil
|
||||
}
|
||||
|
||||
func (v *auth0Validator) Cancel() {}
|
@@ -1,3 +1,34 @@
|
||||
// Package iam implements an identity and access management middleware
|
||||
//
|
||||
// Four information are required in order to decide to grant access.
|
||||
// - identity
|
||||
// - domain
|
||||
// - resource
|
||||
// - action
|
||||
//
|
||||
// The identity of the requester can be obtained by different means:
|
||||
// - JWT
|
||||
// - Username and password in the body as JSON
|
||||
// - Auth0 access token
|
||||
// - Basic auth
|
||||
//
|
||||
// The path prefix /api/login is treated specially in order to accomodate
|
||||
// different ways of identification (UserPass, Auth0). All other /api paths
|
||||
// only allow JWT as authentication method.
|
||||
//
|
||||
// If the identity can't be detected, the identity of "$anon" is given, representing
|
||||
// an anonmyous user. If the request originates from localhost, the identity will
|
||||
// be $localhost, representing an anonymous user from localhost.
|
||||
//
|
||||
// The domain is provided as query parameter "group" for all API requests or the
|
||||
// first path element after a mountpoint for filesystem requests.
|
||||
//
|
||||
// If the domain can't be detected, the domain "$none" will be used.
|
||||
//
|
||||
// The resource is the path of the request. For API requests it's prepended with
|
||||
// the "api:" prefix. For all other requests it's prepended with the "fs:" prefix.
|
||||
//
|
||||
// The action is the requests HTTP method (e.g. GET, POST, ...).
|
||||
package iam
|
||||
|
||||
import (
|
||||
@@ -22,12 +53,14 @@ import (
|
||||
type Config struct {
|
||||
// Skipper defines a function to skip middleware.
|
||||
Skipper middleware.Skipper
|
||||
Mounts []string
|
||||
IAM iam.IAM
|
||||
Logger log.Logger
|
||||
}
|
||||
|
||||
var DefaultConfig = Config{
|
||||
Skipper: middleware.DefaultSkipper,
|
||||
Mounts: []string{},
|
||||
IAM: nil,
|
||||
Logger: nil,
|
||||
}
|
||||
@@ -49,7 +82,7 @@ func NewWithConfig(config Config) echo.MiddlewareFunc {
|
||||
|
||||
mw := iammiddleware{
|
||||
iam: config.IAM,
|
||||
mounts: []string{"/", "/memfs"},
|
||||
mounts: config.Mounts,
|
||||
logger: config.Logger,
|
||||
}
|
||||
|
||||
@@ -62,6 +95,8 @@ func NewWithConfig(config Config) echo.MiddlewareFunc {
|
||||
return false
|
||||
})
|
||||
|
||||
mw.logger.Debug().WithField("mounts", mw.mounts).Log("")
|
||||
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
if config.Skipper(c) {
|
||||
@@ -122,7 +157,7 @@ func NewWithConfig(config Config) echo.MiddlewareFunc {
|
||||
} else {
|
||||
identity, err = mw.findIdentityFromBasicAuth(c)
|
||||
if err != nil {
|
||||
return api.Err(http.StatusForbidden, "Bad request", "%s", err)
|
||||
return api.Err(http.StatusForbidden, "Forbidden", "%s", err)
|
||||
}
|
||||
|
||||
domain = mw.findDomainFromFilesystem(resource)
|
||||
@@ -130,7 +165,13 @@ func NewWithConfig(config Config) echo.MiddlewareFunc {
|
||||
}
|
||||
|
||||
username := "$anon"
|
||||
if identity != nil {
|
||||
if identity == nil {
|
||||
ip := c.RealIP()
|
||||
|
||||
if ip == "127.0.0.1" || ip == "::1" {
|
||||
username = "$localhost"
|
||||
}
|
||||
} else {
|
||||
username = identity.Name()
|
||||
}
|
||||
|
||||
@@ -146,18 +187,8 @@ func NewWithConfig(config Config) echo.MiddlewareFunc {
|
||||
|
||||
action := c.Request().Method
|
||||
|
||||
l := mw.logger.Debug().WithFields(log.Fields{
|
||||
"subject": username,
|
||||
"domain": domain,
|
||||
"resource": resource,
|
||||
"action": action,
|
||||
})
|
||||
|
||||
if ok, rule := config.IAM.Enforce(username, domain, resource, action); !ok {
|
||||
l.Log("access denied")
|
||||
if ok, _ := config.IAM.Enforce(username, domain, resource, action); !ok {
|
||||
return api.Err(http.StatusForbidden, "Forbidden", "access denied")
|
||||
} else {
|
||||
l.Log(rule)
|
||||
}
|
||||
|
||||
return next(c)
|
||||
@@ -203,7 +234,7 @@ func (m *iammiddleware) findIdentityFromBasicAuth(c echo.Context) (iam.IdentityV
|
||||
return nil, fmt.Errorf("invalid username or password")
|
||||
}
|
||||
|
||||
if ok, err := identity.VerifyAPIPassword(password); !ok {
|
||||
if ok, err := identity.VerifyServiceBasicAuth(password); !ok {
|
||||
m.logger.Debug().WithFields(log.Fields{
|
||||
"path": c.Request().URL.Path,
|
||||
"method": c.Request().Method,
|
||||
@@ -373,7 +404,7 @@ func (m *iammiddleware) findDomainFromFilesystem(path string) string {
|
||||
// Remove it from the path and split it into components: foobar file.txt
|
||||
// Check if foobar a known domain. If yes, return it. If not, return empty domain.
|
||||
for _, mount := range m.mounts {
|
||||
prefix := filepath.Join(mount, "/")
|
||||
prefix := filepath.Clean(mount) + "/"
|
||||
if strings.HasPrefix(path, prefix) {
|
||||
elements := strings.Split(strings.TrimPrefix(path, prefix), "/")
|
||||
if m.iam.IsDomain(elements[0]) {
|
||||
|
@@ -213,8 +213,15 @@ func NewServer(config Config) (Server, error) {
|
||||
s.logger = log.New("")
|
||||
}
|
||||
|
||||
mounts := []string{}
|
||||
|
||||
for _, fs := range s.filesystems {
|
||||
mounts = append(mounts, fs.FS.Mountpoint)
|
||||
}
|
||||
|
||||
s.middleware.iam = mwiam.NewWithConfig(mwiam.Config{
|
||||
IAM: config.IAM,
|
||||
Mounts: mounts,
|
||||
Logger: s.logger.WithComponent("IAM"),
|
||||
})
|
||||
|
||||
|
@@ -1,9 +1,11 @@
|
||||
package iam
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/datarhei/core/v16/io/fs"
|
||||
"github.com/datarhei/core/v16/log"
|
||||
|
||||
"github.com/casbin/casbin/v2"
|
||||
"github.com/casbin/casbin/v2/model"
|
||||
@@ -11,23 +13,41 @@ import (
|
||||
|
||||
type AccessEnforcer interface {
|
||||
Enforce(name, domain, resource, action string) (bool, string)
|
||||
HasGroup(name string) bool
|
||||
}
|
||||
|
||||
type AccessManager interface {
|
||||
AccessEnforcer
|
||||
|
||||
AddPolicy()
|
||||
AddPolicy(username, domain, resource, actions string) bool
|
||||
RemovePolicy(username, domain, resource, actions string) bool
|
||||
}
|
||||
|
||||
type access struct {
|
||||
fs fs.Filesystem
|
||||
fs fs.Filesystem
|
||||
logger log.Logger
|
||||
|
||||
adapter *adapter
|
||||
enforcer *casbin.Enforcer
|
||||
}
|
||||
|
||||
func NewAccessManager(fs fs.Filesystem) (AccessManager, error) {
|
||||
type AccessConfig struct {
|
||||
FS fs.Filesystem
|
||||
Logger log.Logger
|
||||
}
|
||||
|
||||
func NewAccessManager(config AccessConfig) (AccessManager, error) {
|
||||
am := &access{
|
||||
fs: fs,
|
||||
fs: config.FS,
|
||||
logger: config.Logger,
|
||||
}
|
||||
|
||||
if am.fs == nil {
|
||||
return nil, fmt.Errorf("a filesystem has to be provided")
|
||||
}
|
||||
|
||||
if am.logger == nil {
|
||||
am.logger = log.New("")
|
||||
}
|
||||
|
||||
m := model.NewModel()
|
||||
@@ -37,7 +57,7 @@ func NewAccessManager(fs fs.Filesystem) (AccessManager, error) {
|
||||
m.AddDef("e", "e", "some(where (p.eft == allow))")
|
||||
m.AddDef("m", "m", `g(r.sub, p.sub, r.dom) && r.dom == p.dom && ResourceMatch(r.obj, r.dom, p.obj) && ActionMatch(r.act, p.act) || r.sub == "$superuser"`)
|
||||
|
||||
a := newAdapter(fs, "./policy.json")
|
||||
a := newAdapter(am.fs, "./policy.json", am.logger)
|
||||
|
||||
e, err := casbin.NewEnforcer(m, a)
|
||||
if err != nil {
|
||||
@@ -48,13 +68,60 @@ func NewAccessManager(fs fs.Filesystem) (AccessManager, error) {
|
||||
e.AddFunction("ActionMatch", actionMatchFunc)
|
||||
|
||||
am.enforcer = e
|
||||
am.adapter = a
|
||||
|
||||
return am, nil
|
||||
}
|
||||
|
||||
func (am *access) AddPolicy() {}
|
||||
func (am *access) AddPolicy(username, domain, resource, actions string) bool {
|
||||
policy := []string{username, domain, resource, actions}
|
||||
|
||||
if am.enforcer.HasPolicy(policy) {
|
||||
return true
|
||||
}
|
||||
|
||||
ok, _ := am.enforcer.AddPolicy(policy)
|
||||
|
||||
return ok
|
||||
}
|
||||
|
||||
func (am *access) RemovePolicy(username, domain, resource, actions string) bool {
|
||||
policies := am.enforcer.GetFilteredPolicy(0, username, domain, resource, actions)
|
||||
am.enforcer.RemovePolicies(policies)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (am *access) HasGroup(name string) bool {
|
||||
groups, err := am.enforcer.GetAllDomains()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, g := range groups {
|
||||
if g == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (am *access) Enforce(name, domain, resource, action string) (bool, string) {
|
||||
l := am.logger.Debug().WithFields(log.Fields{
|
||||
"subject": name,
|
||||
"domain": domain,
|
||||
"resource": resource,
|
||||
"action": action,
|
||||
})
|
||||
|
||||
ok, rule, _ := am.enforcer.EnforceEx(name, domain, resource, action)
|
||||
|
||||
if !ok {
|
||||
l.Log("no match")
|
||||
} else {
|
||||
l.WithField("rule", strings.Join(rule, ", ")).Log("match")
|
||||
}
|
||||
|
||||
return ok, strings.Join(rule, ", ")
|
||||
}
|
||||
|
@@ -8,9 +8,9 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/datarhei/core/v16/io/fs"
|
||||
"github.com/datarhei/core/v16/log"
|
||||
|
||||
"github.com/casbin/casbin/v2/model"
|
||||
"github.com/casbin/casbin/v2/persist"
|
||||
)
|
||||
|
||||
// Adapter is the file adapter for Casbin.
|
||||
@@ -18,12 +18,17 @@ import (
|
||||
type adapter struct {
|
||||
fs fs.Filesystem
|
||||
filePath string
|
||||
logger log.Logger
|
||||
groups []Group
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func newAdapter(fs fs.Filesystem, filePath string) persist.Adapter {
|
||||
return &adapter{filePath: filePath, fs: fs}
|
||||
func newAdapter(fs fs.Filesystem, filePath string, logger log.Logger) *adapter {
|
||||
return &adapter{
|
||||
fs: fs,
|
||||
filePath: filePath,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Adapter
|
||||
@@ -35,13 +40,6 @@ func (a *adapter) LoadPolicy(model model.Model) error {
|
||||
return fmt.Errorf("invalid file path, file path cannot be empty")
|
||||
}
|
||||
|
||||
/*
|
||||
logger := &log.DefaultLogger{}
|
||||
logger.EnableLog(true)
|
||||
|
||||
model.SetLogger(logger)
|
||||
*/
|
||||
|
||||
return a.loadPolicyFile(model)
|
||||
}
|
||||
|
||||
@@ -111,7 +109,12 @@ func (a *adapter) importPolicy(model model.Model, rule []string) error {
|
||||
copiedRule := make([]string, len(rule))
|
||||
copy(copiedRule, rule)
|
||||
|
||||
fmt.Printf("%+v\n", copiedRule)
|
||||
a.logger.Debug().WithFields(log.Fields{
|
||||
"username": copiedRule[1],
|
||||
"domain": copiedRule[2],
|
||||
"resource": copiedRule[3],
|
||||
"actions": copiedRule[4],
|
||||
}).Log("imported policy")
|
||||
|
||||
ok, err := model.HasPolicyEx(copiedRule[0], copiedRule[0], copiedRule[1:])
|
||||
if err != nil {
|
||||
@@ -199,10 +202,23 @@ func (a *adapter) addPolicy(ptype string, rule []string) error {
|
||||
domain = rule[1]
|
||||
resource = rule[2]
|
||||
actions = rule[3]
|
||||
|
||||
a.logger.Debug().WithFields(log.Fields{
|
||||
"username": username,
|
||||
"domain": domain,
|
||||
"resource": resource,
|
||||
"actions": actions,
|
||||
}).Log("adding policy")
|
||||
} else if ptype == "g" {
|
||||
username = rule[0]
|
||||
role = rule[1]
|
||||
domain = rule[2]
|
||||
|
||||
a.logger.Debug().WithFields(log.Fields{
|
||||
"username": username,
|
||||
"role": role,
|
||||
"domain": domain,
|
||||
}).Log("adding role mapping")
|
||||
} else {
|
||||
return fmt.Errorf("unknown ptype: %s", ptype)
|
||||
}
|
||||
@@ -378,10 +394,23 @@ func (a *adapter) removePolicy(ptype string, rule []string) error {
|
||||
domain = rule[1]
|
||||
resource = rule[2]
|
||||
actions = rule[3]
|
||||
|
||||
a.logger.Debug().WithFields(log.Fields{
|
||||
"username": username,
|
||||
"domain": domain,
|
||||
"resource": resource,
|
||||
"actions": actions,
|
||||
}).Log("removing policy")
|
||||
} else if ptype == "g" {
|
||||
username = rule[0]
|
||||
role = rule[1]
|
||||
domain = rule[2]
|
||||
|
||||
a.logger.Debug().WithFields(log.Fields{
|
||||
"username": username,
|
||||
"role": role,
|
||||
"domain": domain,
|
||||
}).Log("adding role mapping")
|
||||
} else {
|
||||
return fmt.Errorf("unknown ptype: %s", ptype)
|
||||
}
|
||||
@@ -451,19 +480,6 @@ func (a *adapter) RemoveFilteredPolicy(sec string, ptype string, fieldIndex int,
|
||||
return fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
func (a *adapter) GetAllGroupNames() []string {
|
||||
a.lock.Lock()
|
||||
defer a.lock.Unlock()
|
||||
|
||||
groups := []string{}
|
||||
|
||||
for _, group := range a.groups {
|
||||
groups = append(groups, group.Name)
|
||||
}
|
||||
|
||||
return groups
|
||||
}
|
||||
|
||||
type Group struct {
|
||||
Name string `json:"name"`
|
||||
Roles map[string][]Role `json:"roles"`
|
||||
|
@@ -1,7 +1,6 @@
|
||||
package iam
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gobwas/glob"
|
||||
@@ -14,12 +13,12 @@ func resourceMatch(request, domain, policy string) bool {
|
||||
if reqPrefix != polPrefix {
|
||||
return false
|
||||
}
|
||||
|
||||
fmt.Printf("prefix: %s\n", reqPrefix)
|
||||
fmt.Printf("requested resource: %s\n", reqResource)
|
||||
fmt.Printf("requested domain: %s\n", domain)
|
||||
fmt.Printf("policy resource: %s\n", polResource)
|
||||
|
||||
/*
|
||||
fmt.Printf("prefix: %s\n", reqPrefix)
|
||||
fmt.Printf("requested resource: %s\n", reqResource)
|
||||
fmt.Printf("requested domain: %s\n", domain)
|
||||
fmt.Printf("policy resource: %s\n", polResource)
|
||||
*/
|
||||
var match bool
|
||||
var err error
|
||||
|
||||
@@ -55,7 +54,7 @@ func resourceMatch(request, domain, policy string) bool {
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("match: %v\n", match)
|
||||
//fmt.Printf("match: %v\n", match)
|
||||
|
||||
return match
|
||||
}
|
||||
|
27
iam/iam.go
27
iam/iam.go
@@ -1,11 +1,17 @@
|
||||
package iam
|
||||
|
||||
import "github.com/datarhei/core/v16/io/fs"
|
||||
import (
|
||||
"github.com/datarhei/core/v16/io/fs"
|
||||
"github.com/datarhei/core/v16/log"
|
||||
)
|
||||
|
||||
type IAM interface {
|
||||
Enforce(user, domain, resource, action string) (bool, string)
|
||||
IsDomain(domain string) bool
|
||||
|
||||
AddPolicy(username, domain, resource, actions string) bool
|
||||
RemovePolicy(username, domain, resource, actions string) bool
|
||||
|
||||
Validators() []string
|
||||
|
||||
GetIdentity(name string) (IdentityVerifier, error)
|
||||
@@ -25,20 +31,27 @@ type iam struct {
|
||||
type Config struct {
|
||||
FS fs.Filesystem
|
||||
Superuser User
|
||||
JWTRealm string
|
||||
JWTSecret string
|
||||
Logger log.Logger
|
||||
}
|
||||
|
||||
func NewIAM(config Config) (IAM, error) {
|
||||
im, err := NewIdentityManager(IdentityConfig{
|
||||
FS: config.FS,
|
||||
Superuser: config.Superuser,
|
||||
JWTRealm: config.JWTRealm,
|
||||
JWTSecret: config.JWTSecret,
|
||||
Logger: config.Logger,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
am, err := NewAccessManager(config.FS)
|
||||
am, err := NewAccessManager(AccessConfig{
|
||||
FS: config.FS,
|
||||
Logger: config.Logger,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -79,9 +92,17 @@ func (i *iam) CreateJWT(name string) (string, string, error) {
|
||||
}
|
||||
|
||||
func (i *iam) IsDomain(domain string) bool {
|
||||
return false
|
||||
return i.am.HasGroup(domain)
|
||||
}
|
||||
|
||||
func (i *iam) Validators() []string {
|
||||
return i.im.Validators()
|
||||
}
|
||||
|
||||
func (i *iam) AddPolicy(username, domain, resource, actions string) bool {
|
||||
return i.am.AddPolicy(username, domain, resource, actions)
|
||||
}
|
||||
|
||||
func (i *iam) RemovePolicy(username, domain, resource, actions string) bool {
|
||||
return i.am.RemovePolicy(username, domain, resource, actions)
|
||||
}
|
||||
|
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/datarhei/core/v16/iam/jwks"
|
||||
"github.com/datarhei/core/v16/io/fs"
|
||||
"github.com/datarhei/core/v16/log"
|
||||
"github.com/google/uuid"
|
||||
|
||||
jwtgo "github.com/golang-jwt/jwt/v4"
|
||||
@@ -44,7 +45,7 @@ type UserAuthAPIAuth0 struct {
|
||||
|
||||
type UserAuthServices struct {
|
||||
Basic UserAuthPassword `json:"basic"`
|
||||
Token string `json:"token"`
|
||||
Token []string `json:"token"`
|
||||
}
|
||||
|
||||
type UserAuthPassword struct {
|
||||
@@ -88,7 +89,9 @@ func (u *User) marshalIdentity() *identity {
|
||||
type identity struct {
|
||||
user User
|
||||
|
||||
tenant *auth0Tenant
|
||||
tenant *auth0Tenant
|
||||
|
||||
jwtRealm string
|
||||
jwtKeyFunc func(*jwtgo.Token) (interface{}, error)
|
||||
|
||||
valid bool
|
||||
@@ -231,7 +234,7 @@ func (i *identity) VerifyJWT(jwt string) (bool, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if issuer != "datarhei-core" {
|
||||
if issuer != i.jwtRealm {
|
||||
return false, fmt.Errorf("wrong issuer")
|
||||
}
|
||||
|
||||
@@ -274,7 +277,13 @@ func (i *identity) VerifyServiceToken(token string) (bool, error) {
|
||||
return false, fmt.Errorf("invalid identity")
|
||||
}
|
||||
|
||||
return i.user.Auth.Services.Token == token, nil
|
||||
for _, t := range i.user.Auth.Services.Token {
|
||||
if t == token {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (i *identity) isValid() bool {
|
||||
@@ -329,7 +338,9 @@ type identityManager struct {
|
||||
|
||||
fs fs.Filesystem
|
||||
filePath string
|
||||
logger log.Logger
|
||||
|
||||
jwtRealm string
|
||||
jwtSecret []byte
|
||||
|
||||
lock sync.RWMutex
|
||||
@@ -338,7 +349,9 @@ type identityManager struct {
|
||||
type IdentityConfig struct {
|
||||
FS fs.Filesystem
|
||||
Superuser User
|
||||
JWTRealm string
|
||||
JWTSecret string
|
||||
Logger log.Logger
|
||||
}
|
||||
|
||||
func NewIdentityManager(config IdentityConfig) (IdentityManager, error) {
|
||||
@@ -348,7 +361,13 @@ func NewIdentityManager(config IdentityConfig) (IdentityManager, error) {
|
||||
auth0UserIdentityMap: map[string]string{},
|
||||
fs: config.FS,
|
||||
filePath: "./users.json",
|
||||
jwtRealm: config.JWTRealm,
|
||||
jwtSecret: []byte(config.JWTSecret),
|
||||
logger: config.Logger,
|
||||
}
|
||||
|
||||
if im.logger == nil {
|
||||
im.logger = log.New("")
|
||||
}
|
||||
|
||||
err := im.load(im.filePath)
|
||||
@@ -470,6 +489,7 @@ func (im *identityManager) getIdentity(name string) (*identity, error) {
|
||||
return nil, fmt.Errorf("not found")
|
||||
}
|
||||
|
||||
identity.jwtRealm = im.jwtRealm
|
||||
identity.jwtKeyFunc = func(*jwtgo.Token) (interface{}, error) { return im.jwtSecret, nil }
|
||||
|
||||
return identity, nil
|
||||
@@ -616,7 +636,7 @@ func (im *identityManager) CreateJWT(name string) (string, string, error) {
|
||||
|
||||
// Create access token
|
||||
accessToken := jwtgo.NewWithClaims(jwtgo.SigningMethodHS256, jwtgo.MapClaims{
|
||||
"iss": "datarhei-core",
|
||||
"iss": im.jwtRealm,
|
||||
"sub": name,
|
||||
"usefor": "access",
|
||||
"iat": now.Unix(),
|
||||
@@ -633,7 +653,7 @@ func (im *identityManager) CreateJWT(name string) (string, string, error) {
|
||||
|
||||
// Create refresh token
|
||||
refreshToken := jwtgo.NewWithClaims(jwtgo.SigningMethodHS256, jwtgo.MapClaims{
|
||||
"iss": "datarhei-core",
|
||||
"iss": im.jwtRealm,
|
||||
"sub": name,
|
||||
"usefor": "refresh",
|
||||
"iat": now.Unix(),
|
||||
|
@@ -43,8 +43,8 @@ type File interface {
|
||||
}
|
||||
|
||||
type ReadFilesystem interface {
|
||||
// Size returns the consumed size and capacity of the filesystem in bytes. The
|
||||
// capacity is negative if the filesystem can consume as much space as it wants.
|
||||
// Size returns the consumed size and capacity of the filesystem in bytes. If the
|
||||
// capacity is 0 or smaller if the filesystem can consume as much space as it wants.
|
||||
Size() (int64, int64)
|
||||
|
||||
// Files returns the current number of files in the filesystem.
|
||||
|
@@ -67,7 +67,7 @@ func (r *sizedFilesystem) Resize(size int64) error {
|
||||
|
||||
func (r *sizedFilesystem) WriteFileReader(path string, rd io.Reader) (int64, bool, error) {
|
||||
currentSize, maxSize := r.Size()
|
||||
if maxSize < 0 {
|
||||
if maxSize <= 0 {
|
||||
return r.Filesystem.WriteFileReader(path, rd)
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user