Define default policies to mimic current behaviour

This commit is contained in:
Ingo Oppermann
2023-02-10 15:14:30 +01:00
parent 312f65d110
commit eac49ad11a
13 changed files with 259 additions and 540 deletions

View File

@@ -400,7 +400,10 @@ func (a *api) start() error {
Enable: cfg.Storage.Memory.Auth.Enable, Enable: cfg.Storage.Memory.Auth.Enable,
Password: cfg.Storage.Memory.Auth.Password, 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 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{ iam, err := iam.NewIAM(iam.Config{
FS: fs, FS: fs,
Superuser: superuser, Superuser: superuser,
JWTRealm: "datarhei-core",
JWTSecret: secret, JWTSecret: secret,
Logger: a.log.logger.core.WithComponent("IAM"),
}) })
if err != nil { if err != nil {
return fmt.Errorf("iam: %w", err) 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 a.iam = iam
} }

View File

@@ -84,7 +84,7 @@ func (h *FSHandler) PutFile(c echo.Context) error {
_, created, err := h.fs.Filesystem.WriteFileReader(path, req.Body) _, created, err := h.fs.Filesystem.WriteFileReader(path, req.Body)
if err != nil { if err != nil {
return api.Err(http.StatusBadRequest, "%s", err) return api.Err(http.StatusBadRequest, "Bad request", "%s", err)
} }
if h.fs.Cache != nil { if h.fs.Cache != nil {

View File

@@ -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
}

View File

@@ -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() {}

View File

@@ -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 package iam
import ( import (
@@ -22,12 +53,14 @@ import (
type Config struct { type Config struct {
// Skipper defines a function to skip middleware. // Skipper defines a function to skip middleware.
Skipper middleware.Skipper Skipper middleware.Skipper
Mounts []string
IAM iam.IAM IAM iam.IAM
Logger log.Logger Logger log.Logger
} }
var DefaultConfig = Config{ var DefaultConfig = Config{
Skipper: middleware.DefaultSkipper, Skipper: middleware.DefaultSkipper,
Mounts: []string{},
IAM: nil, IAM: nil,
Logger: nil, Logger: nil,
} }
@@ -49,7 +82,7 @@ func NewWithConfig(config Config) echo.MiddlewareFunc {
mw := iammiddleware{ mw := iammiddleware{
iam: config.IAM, iam: config.IAM,
mounts: []string{"/", "/memfs"}, mounts: config.Mounts,
logger: config.Logger, logger: config.Logger,
} }
@@ -62,6 +95,8 @@ func NewWithConfig(config Config) echo.MiddlewareFunc {
return false return false
}) })
mw.logger.Debug().WithField("mounts", mw.mounts).Log("")
return func(next echo.HandlerFunc) echo.HandlerFunc { return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
if config.Skipper(c) { if config.Skipper(c) {
@@ -122,7 +157,7 @@ func NewWithConfig(config Config) echo.MiddlewareFunc {
} else { } else {
identity, err = mw.findIdentityFromBasicAuth(c) identity, err = mw.findIdentityFromBasicAuth(c)
if err != nil { if err != nil {
return api.Err(http.StatusForbidden, "Bad request", "%s", err) return api.Err(http.StatusForbidden, "Forbidden", "%s", err)
} }
domain = mw.findDomainFromFilesystem(resource) domain = mw.findDomainFromFilesystem(resource)
@@ -130,7 +165,13 @@ func NewWithConfig(config Config) echo.MiddlewareFunc {
} }
username := "$anon" 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() username = identity.Name()
} }
@@ -146,18 +187,8 @@ func NewWithConfig(config Config) echo.MiddlewareFunc {
action := c.Request().Method action := c.Request().Method
l := mw.logger.Debug().WithFields(log.Fields{ if ok, _ := config.IAM.Enforce(username, domain, resource, action); !ok {
"subject": username,
"domain": domain,
"resource": resource,
"action": action,
})
if ok, rule := config.IAM.Enforce(username, domain, resource, action); !ok {
l.Log("access denied")
return api.Err(http.StatusForbidden, "Forbidden", "access denied") return api.Err(http.StatusForbidden, "Forbidden", "access denied")
} else {
l.Log(rule)
} }
return next(c) return next(c)
@@ -203,7 +234,7 @@ func (m *iammiddleware) findIdentityFromBasicAuth(c echo.Context) (iam.IdentityV
return nil, fmt.Errorf("invalid username or password") 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{ m.logger.Debug().WithFields(log.Fields{
"path": c.Request().URL.Path, "path": c.Request().URL.Path,
"method": c.Request().Method, "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 // 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. // Check if foobar a known domain. If yes, return it. If not, return empty domain.
for _, mount := range m.mounts { for _, mount := range m.mounts {
prefix := filepath.Join(mount, "/") prefix := filepath.Clean(mount) + "/"
if strings.HasPrefix(path, prefix) { if strings.HasPrefix(path, prefix) {
elements := strings.Split(strings.TrimPrefix(path, prefix), "/") elements := strings.Split(strings.TrimPrefix(path, prefix), "/")
if m.iam.IsDomain(elements[0]) { if m.iam.IsDomain(elements[0]) {

View File

@@ -213,8 +213,15 @@ func NewServer(config Config) (Server, error) {
s.logger = log.New("") s.logger = log.New("")
} }
mounts := []string{}
for _, fs := range s.filesystems {
mounts = append(mounts, fs.FS.Mountpoint)
}
s.middleware.iam = mwiam.NewWithConfig(mwiam.Config{ s.middleware.iam = mwiam.NewWithConfig(mwiam.Config{
IAM: config.IAM, IAM: config.IAM,
Mounts: mounts,
Logger: s.logger.WithComponent("IAM"), Logger: s.logger.WithComponent("IAM"),
}) })

View File

@@ -1,9 +1,11 @@
package iam package iam
import ( import (
"fmt"
"strings" "strings"
"github.com/datarhei/core/v16/io/fs" "github.com/datarhei/core/v16/io/fs"
"github.com/datarhei/core/v16/log"
"github.com/casbin/casbin/v2" "github.com/casbin/casbin/v2"
"github.com/casbin/casbin/v2/model" "github.com/casbin/casbin/v2/model"
@@ -11,23 +13,41 @@ import (
type AccessEnforcer interface { type AccessEnforcer interface {
Enforce(name, domain, resource, action string) (bool, string) Enforce(name, domain, resource, action string) (bool, string)
HasGroup(name string) bool
} }
type AccessManager interface { type AccessManager interface {
AccessEnforcer AccessEnforcer
AddPolicy() AddPolicy(username, domain, resource, actions string) bool
RemovePolicy(username, domain, resource, actions string) bool
} }
type access struct { type access struct {
fs fs.Filesystem fs fs.Filesystem
logger log.Logger
adapter *adapter
enforcer *casbin.Enforcer 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{ 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() 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("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"`) 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) e, err := casbin.NewEnforcer(m, a)
if err != nil { if err != nil {
@@ -48,13 +68,60 @@ func NewAccessManager(fs fs.Filesystem) (AccessManager, error) {
e.AddFunction("ActionMatch", actionMatchFunc) e.AddFunction("ActionMatch", actionMatchFunc)
am.enforcer = e am.enforcer = e
am.adapter = a
return am, nil 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) { 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) 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, ", ") return ok, strings.Join(rule, ", ")
} }

View File

@@ -8,9 +8,9 @@ import (
"sync" "sync"
"github.com/datarhei/core/v16/io/fs" "github.com/datarhei/core/v16/io/fs"
"github.com/datarhei/core/v16/log"
"github.com/casbin/casbin/v2/model" "github.com/casbin/casbin/v2/model"
"github.com/casbin/casbin/v2/persist"
) )
// Adapter is the file adapter for Casbin. // Adapter is the file adapter for Casbin.
@@ -18,12 +18,17 @@ import (
type adapter struct { type adapter struct {
fs fs.Filesystem fs fs.Filesystem
filePath string filePath string
logger log.Logger
groups []Group groups []Group
lock sync.Mutex lock sync.Mutex
} }
func newAdapter(fs fs.Filesystem, filePath string) persist.Adapter { func newAdapter(fs fs.Filesystem, filePath string, logger log.Logger) *adapter {
return &adapter{filePath: filePath, fs: fs} return &adapter{
fs: fs,
filePath: filePath,
logger: logger,
}
} }
// Adapter // Adapter
@@ -35,13 +40,6 @@ func (a *adapter) LoadPolicy(model model.Model) error {
return fmt.Errorf("invalid file path, file path cannot be empty") return fmt.Errorf("invalid file path, file path cannot be empty")
} }
/*
logger := &log.DefaultLogger{}
logger.EnableLog(true)
model.SetLogger(logger)
*/
return a.loadPolicyFile(model) return a.loadPolicyFile(model)
} }
@@ -111,7 +109,12 @@ func (a *adapter) importPolicy(model model.Model, rule []string) error {
copiedRule := make([]string, len(rule)) copiedRule := make([]string, len(rule))
copy(copiedRule, 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:]) ok, err := model.HasPolicyEx(copiedRule[0], copiedRule[0], copiedRule[1:])
if err != nil { if err != nil {
@@ -199,10 +202,23 @@ func (a *adapter) addPolicy(ptype string, rule []string) error {
domain = rule[1] domain = rule[1]
resource = rule[2] resource = rule[2]
actions = rule[3] actions = rule[3]
a.logger.Debug().WithFields(log.Fields{
"username": username,
"domain": domain,
"resource": resource,
"actions": actions,
}).Log("adding policy")
} else if ptype == "g" { } else if ptype == "g" {
username = rule[0] username = rule[0]
role = rule[1] role = rule[1]
domain = rule[2] domain = rule[2]
a.logger.Debug().WithFields(log.Fields{
"username": username,
"role": role,
"domain": domain,
}).Log("adding role mapping")
} else { } else {
return fmt.Errorf("unknown ptype: %s", ptype) return fmt.Errorf("unknown ptype: %s", ptype)
} }
@@ -378,10 +394,23 @@ func (a *adapter) removePolicy(ptype string, rule []string) error {
domain = rule[1] domain = rule[1]
resource = rule[2] resource = rule[2]
actions = rule[3] actions = rule[3]
a.logger.Debug().WithFields(log.Fields{
"username": username,
"domain": domain,
"resource": resource,
"actions": actions,
}).Log("removing policy")
} else if ptype == "g" { } else if ptype == "g" {
username = rule[0] username = rule[0]
role = rule[1] role = rule[1]
domain = rule[2] domain = rule[2]
a.logger.Debug().WithFields(log.Fields{
"username": username,
"role": role,
"domain": domain,
}).Log("adding role mapping")
} else { } else {
return fmt.Errorf("unknown ptype: %s", ptype) 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") 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 { type Group struct {
Name string `json:"name"` Name string `json:"name"`
Roles map[string][]Role `json:"roles"` Roles map[string][]Role `json:"roles"`

View File

@@ -1,7 +1,6 @@
package iam package iam
import ( import (
"fmt"
"strings" "strings"
"github.com/gobwas/glob" "github.com/gobwas/glob"
@@ -14,12 +13,12 @@ func resourceMatch(request, domain, policy string) bool {
if reqPrefix != polPrefix { if reqPrefix != polPrefix {
return false return false
} }
/*
fmt.Printf("prefix: %s\n", reqPrefix) fmt.Printf("prefix: %s\n", reqPrefix)
fmt.Printf("requested resource: %s\n", reqResource) fmt.Printf("requested resource: %s\n", reqResource)
fmt.Printf("requested domain: %s\n", domain) fmt.Printf("requested domain: %s\n", domain)
fmt.Printf("policy resource: %s\n", polResource) fmt.Printf("policy resource: %s\n", polResource)
*/
var match bool var match bool
var err error 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 return match
} }

View File

@@ -1,11 +1,17 @@
package iam 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 { type IAM interface {
Enforce(user, domain, resource, action string) (bool, string) Enforce(user, domain, resource, action string) (bool, string)
IsDomain(domain string) bool IsDomain(domain string) bool
AddPolicy(username, domain, resource, actions string) bool
RemovePolicy(username, domain, resource, actions string) bool
Validators() []string Validators() []string
GetIdentity(name string) (IdentityVerifier, error) GetIdentity(name string) (IdentityVerifier, error)
@@ -25,20 +31,27 @@ type iam struct {
type Config struct { type Config struct {
FS fs.Filesystem FS fs.Filesystem
Superuser User Superuser User
JWTRealm string
JWTSecret string JWTSecret string
Logger log.Logger
} }
func NewIAM(config Config) (IAM, error) { func NewIAM(config Config) (IAM, error) {
im, err := NewIdentityManager(IdentityConfig{ im, err := NewIdentityManager(IdentityConfig{
FS: config.FS, FS: config.FS,
Superuser: config.Superuser, Superuser: config.Superuser,
JWTRealm: config.JWTRealm,
JWTSecret: config.JWTSecret, JWTSecret: config.JWTSecret,
Logger: config.Logger,
}) })
if err != nil { if err != nil {
return nil, err return nil, err
} }
am, err := NewAccessManager(config.FS) am, err := NewAccessManager(AccessConfig{
FS: config.FS,
Logger: config.Logger,
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -79,9 +92,17 @@ func (i *iam) CreateJWT(name string) (string, string, error) {
} }
func (i *iam) IsDomain(domain string) bool { func (i *iam) IsDomain(domain string) bool {
return false return i.am.HasGroup(domain)
} }
func (i *iam) Validators() []string { func (i *iam) Validators() []string {
return i.im.Validators() 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)
}

View File

@@ -10,6 +10,7 @@ import (
"github.com/datarhei/core/v16/iam/jwks" "github.com/datarhei/core/v16/iam/jwks"
"github.com/datarhei/core/v16/io/fs" "github.com/datarhei/core/v16/io/fs"
"github.com/datarhei/core/v16/log"
"github.com/google/uuid" "github.com/google/uuid"
jwtgo "github.com/golang-jwt/jwt/v4" jwtgo "github.com/golang-jwt/jwt/v4"
@@ -44,7 +45,7 @@ type UserAuthAPIAuth0 struct {
type UserAuthServices struct { type UserAuthServices struct {
Basic UserAuthPassword `json:"basic"` Basic UserAuthPassword `json:"basic"`
Token string `json:"token"` Token []string `json:"token"`
} }
type UserAuthPassword struct { type UserAuthPassword struct {
@@ -89,6 +90,8 @@ type identity struct {
user User user User
tenant *auth0Tenant tenant *auth0Tenant
jwtRealm string
jwtKeyFunc func(*jwtgo.Token) (interface{}, error) jwtKeyFunc func(*jwtgo.Token) (interface{}, error)
valid bool 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") 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 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 { func (i *identity) isValid() bool {
@@ -329,7 +338,9 @@ type identityManager struct {
fs fs.Filesystem fs fs.Filesystem
filePath string filePath string
logger log.Logger
jwtRealm string
jwtSecret []byte jwtSecret []byte
lock sync.RWMutex lock sync.RWMutex
@@ -338,7 +349,9 @@ type identityManager struct {
type IdentityConfig struct { type IdentityConfig struct {
FS fs.Filesystem FS fs.Filesystem
Superuser User Superuser User
JWTRealm string
JWTSecret string JWTSecret string
Logger log.Logger
} }
func NewIdentityManager(config IdentityConfig) (IdentityManager, error) { func NewIdentityManager(config IdentityConfig) (IdentityManager, error) {
@@ -348,7 +361,13 @@ func NewIdentityManager(config IdentityConfig) (IdentityManager, error) {
auth0UserIdentityMap: map[string]string{}, auth0UserIdentityMap: map[string]string{},
fs: config.FS, fs: config.FS,
filePath: "./users.json", filePath: "./users.json",
jwtRealm: config.JWTRealm,
jwtSecret: []byte(config.JWTSecret), jwtSecret: []byte(config.JWTSecret),
logger: config.Logger,
}
if im.logger == nil {
im.logger = log.New("")
} }
err := im.load(im.filePath) err := im.load(im.filePath)
@@ -470,6 +489,7 @@ func (im *identityManager) getIdentity(name string) (*identity, error) {
return nil, fmt.Errorf("not found") return nil, fmt.Errorf("not found")
} }
identity.jwtRealm = im.jwtRealm
identity.jwtKeyFunc = func(*jwtgo.Token) (interface{}, error) { return im.jwtSecret, nil } identity.jwtKeyFunc = func(*jwtgo.Token) (interface{}, error) { return im.jwtSecret, nil }
return identity, nil return identity, nil
@@ -616,7 +636,7 @@ func (im *identityManager) CreateJWT(name string) (string, string, error) {
// Create access token // Create access token
accessToken := jwtgo.NewWithClaims(jwtgo.SigningMethodHS256, jwtgo.MapClaims{ accessToken := jwtgo.NewWithClaims(jwtgo.SigningMethodHS256, jwtgo.MapClaims{
"iss": "datarhei-core", "iss": im.jwtRealm,
"sub": name, "sub": name,
"usefor": "access", "usefor": "access",
"iat": now.Unix(), "iat": now.Unix(),
@@ -633,7 +653,7 @@ func (im *identityManager) CreateJWT(name string) (string, string, error) {
// Create refresh token // Create refresh token
refreshToken := jwtgo.NewWithClaims(jwtgo.SigningMethodHS256, jwtgo.MapClaims{ refreshToken := jwtgo.NewWithClaims(jwtgo.SigningMethodHS256, jwtgo.MapClaims{
"iss": "datarhei-core", "iss": im.jwtRealm,
"sub": name, "sub": name,
"usefor": "refresh", "usefor": "refresh",
"iat": now.Unix(), "iat": now.Unix(),

View File

@@ -43,8 +43,8 @@ type File interface {
} }
type ReadFilesystem interface { type ReadFilesystem interface {
// Size returns the consumed size and capacity of the filesystem in bytes. The // Size returns the consumed size and capacity of the filesystem in bytes. If the
// capacity is negative if the filesystem can consume as much space as it wants. // capacity is 0 or smaller if the filesystem can consume as much space as it wants.
Size() (int64, int64) Size() (int64, int64)
// Files returns the current number of files in the filesystem. // Files returns the current number of files in the filesystem.

View File

@@ -67,7 +67,7 @@ func (r *sizedFilesystem) Resize(size int64) error {
func (r *sizedFilesystem) WriteFileReader(path string, rd io.Reader) (int64, bool, error) { func (r *sizedFilesystem) WriteFileReader(path string, rd io.Reader) (int64, bool, error) {
currentSize, maxSize := r.Size() currentSize, maxSize := r.Size()
if maxSize < 0 { if maxSize <= 0 {
return r.Filesystem.WriteFileReader(path, rd) return r.Filesystem.WriteFileReader(path, rd)
} }