mirror of
https://github.com/datarhei/core.git
synced 2025-10-05 07:57:13 +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,
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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 {
|
||||||
|
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
|
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]) {
|
||||||
|
@@ -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"),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@@ -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, ", ")
|
||||||
}
|
}
|
||||||
|
@@ -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"`
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
27
iam/iam.go
27
iam/iam.go
@@ -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)
|
||||||
|
}
|
||||||
|
@@ -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 {
|
||||||
@@ -88,7 +89,9 @@ func (u *User) marshalIdentity() *identity {
|
|||||||
type identity struct {
|
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(),
|
||||||
|
@@ -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.
|
||||||
|
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user