diff --git a/app/api/api.go b/app/api/api.go index 4bd610f9..8937b74e 100644 --- a/app/api/api.go +++ b/app/api/api.go @@ -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 } diff --git a/http/handler/filesystem.go b/http/handler/filesystem.go index 73adde44..a8277e7c 100644 --- a/http/handler/filesystem.go +++ b/http/handler/filesystem.go @@ -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 { diff --git a/http/jwt/jwt.go b/http/jwt/jwt.go deleted file mode 100644 index 2b2dc716..00000000 --- a/http/jwt/jwt.go +++ /dev/null @@ -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 -} diff --git a/http/jwt/validator.go b/http/jwt/validator.go deleted file mode 100644 index 60d02937..00000000 --- a/http/jwt/validator.go +++ /dev/null @@ -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() {} diff --git a/http/middleware/iam/iam.go b/http/middleware/iam/iam.go index 18c3b662..0bfa05a8 100644 --- a/http/middleware/iam/iam.go +++ b/http/middleware/iam/iam.go @@ -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]) { diff --git a/http/server.go b/http/server.go index 6cc0453a..3cdd7a5d 100644 --- a/http/server.go +++ b/http/server.go @@ -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"), }) diff --git a/iam/access.go b/iam/access.go index bf6ce031..f5f9269f 100644 --- a/iam/access.go +++ b/iam/access.go @@ -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, ", ") } diff --git a/iam/adapter.go b/iam/adapter.go index 0aeebad9..7232dcc0 100644 --- a/iam/adapter.go +++ b/iam/adapter.go @@ -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"` diff --git a/iam/casbin.go b/iam/casbin.go index 3d175115..9a402d2b 100644 --- a/iam/casbin.go +++ b/iam/casbin.go @@ -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 } diff --git a/iam/iam.go b/iam/iam.go index 185ca4ef..ae57a47a 100644 --- a/iam/iam.go +++ b/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) +} diff --git a/iam/identity.go b/iam/identity.go index eb25bb1e..c9c500d8 100644 --- a/iam/identity.go +++ b/iam/identity.go @@ -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(), diff --git a/io/fs/fs.go b/io/fs/fs.go index 2977e178..f7b990b3 100644 --- a/io/fs/fs.go +++ b/io/fs/fs.go @@ -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. diff --git a/io/fs/sized.go b/io/fs/sized.go index 1a87ac18..e97bcbb8 100644 --- a/io/fs/sized.go +++ b/io/fs/sized.go @@ -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) }