Create identity and access packages for IAM

This commit is contained in:
Ingo Oppermann
2023-05-25 16:16:29 +02:00
parent 710d5c595f
commit e9034aa171
22 changed files with 514 additions and 286 deletions

View File

@@ -27,6 +27,8 @@ import (
httpfs "github.com/datarhei/core/v16/http/fs" httpfs "github.com/datarhei/core/v16/http/fs"
"github.com/datarhei/core/v16/http/router" "github.com/datarhei/core/v16/http/router"
"github.com/datarhei/core/v16/iam" "github.com/datarhei/core/v16/iam"
iamaccess "github.com/datarhei/core/v16/iam/access"
iamidentity "github.com/datarhei/core/v16/iam/identity"
"github.com/datarhei/core/v16/io/fs" "github.com/datarhei/core/v16/io/fs"
"github.com/datarhei/core/v16/log" "github.com/datarhei/core/v16/log"
"github.com/datarhei/core/v16/math/rand" "github.com/datarhei/core/v16/math/rand"
@@ -390,14 +392,14 @@ func (a *api) start() error {
} }
{ {
superuser := iam.User{ superuser := iamidentity.User{
Name: cfg.API.Auth.Username, Name: cfg.API.Auth.Username,
Superuser: true, Superuser: true,
Auth: iam.UserAuth{ Auth: iamidentity.UserAuth{
API: iam.UserAuthAPI{ API: iamidentity.UserAuthAPI{
Auth0: iam.UserAuthAPIAuth0{}, Auth0: iamidentity.UserAuthAPIAuth0{},
}, },
Services: iam.UserAuthServices{ Services: iamidentity.UserAuthServices{
Token: []string{ Token: []string{
cfg.RTMP.Token, cfg.RTMP.Token,
cfg.SRT.Token, cfg.SRT.Token,
@@ -412,7 +414,7 @@ func (a *api) start() error {
if cfg.API.Auth.Auth0.Enable { if cfg.API.Auth.Auth0.Enable {
superuser.Auth.API.Auth0.User = cfg.API.Auth.Auth0.Tenants[0].Users[0] superuser.Auth.API.Auth0.User = cfg.API.Auth.Auth0.Tenants[0].Users[0]
superuser.Auth.API.Auth0.Tenant = iam.Auth0Tenant{ superuser.Auth.API.Auth0.Tenant = iamidentity.Auth0Tenant{
Domain: cfg.API.Auth.Auth0.Tenants[0].Domain, Domain: cfg.API.Auth.Auth0.Tenants[0].Domain,
Audience: cfg.API.Auth.Auth0.Tenants[0].Audience, Audience: cfg.API.Auth.Auth0.Tenants[0].Audience,
ClientID: cfg.API.Auth.Auth0.Tenants[0].ClientID, ClientID: cfg.API.Auth.Auth0.Tenants[0].ClientID,
@@ -431,12 +433,23 @@ 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
} }
policyAdapter, err := iamaccess.NewJSONAdapter(fs, "./policy.json", nil)
if err != nil {
return err
}
identityAdapter, err := iamidentity.NewJSONAdapter(fs, "./users.json", nil)
if err != nil {
return err
}
manager, err := iam.NewIAM(iam.Config{ manager, err := iam.NewIAM(iam.Config{
FS: fs, PolicyAdapter: policyAdapter,
Superuser: superuser, IdentityAdapter: identityAdapter,
JWTRealm: "datarhei-core", Superuser: superuser,
JWTSecret: secret, JWTRealm: "datarhei-core",
Logger: a.log.logger.core.WithComponent("IAM"), 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)
@@ -445,7 +458,7 @@ func (a *api) start() error {
// Check if there are already file created by IAM. If not, create policies // Check if there are already file created by IAM. If not, create policies
// and users based on the config in order to mimic the behaviour before IAM. // and users based on the config in order to mimic the behaviour before IAM.
if len(fs.List("/", "/*.json")) == 0 { if len(fs.List("/", "/*.json")) == 0 {
policies := []iam.Policy{ policies := []iamaccess.Policy{
{ {
Name: "$anon", Name: "$anon",
Domain: "$none", Domain: "$none",
@@ -466,19 +479,19 @@ func (a *api) start() error {
}, },
} }
users := map[string]iam.User{} users := map[string]iamidentity.User{}
if cfg.Storage.Memory.Auth.Enable && cfg.Storage.Memory.Auth.Username != superuser.Name { if cfg.Storage.Memory.Auth.Enable && cfg.Storage.Memory.Auth.Username != superuser.Name {
users[cfg.Storage.Memory.Auth.Username] = iam.User{ users[cfg.Storage.Memory.Auth.Username] = iamidentity.User{
Name: cfg.Storage.Memory.Auth.Username, Name: cfg.Storage.Memory.Auth.Username,
Auth: iam.UserAuth{ Auth: iamidentity.UserAuth{
Services: iam.UserAuthServices{ Services: iamidentity.UserAuthServices{
Basic: []string{cfg.Storage.Memory.Auth.Password}, Basic: []string{cfg.Storage.Memory.Auth.Password},
}, },
}, },
} }
policies = append(policies, iam.Policy{ policies = append(policies, iamaccess.Policy{
Name: cfg.Storage.Memory.Auth.Username, Name: cfg.Storage.Memory.Auth.Username,
Domain: "$none", Domain: "$none",
Resource: "fs:/memfs/**", Resource: "fs:/memfs/**",
@@ -490,10 +503,10 @@ func (a *api) start() error {
if s.Auth.Enable && s.Auth.Username != superuser.Name { if s.Auth.Enable && s.Auth.Username != superuser.Name {
user, ok := users[s.Auth.Username] user, ok := users[s.Auth.Username]
if !ok { if !ok {
users[s.Auth.Username] = iam.User{ users[s.Auth.Username] = iamidentity.User{
Name: s.Auth.Username, Name: s.Auth.Username,
Auth: iam.UserAuth{ Auth: iamidentity.UserAuth{
Services: iam.UserAuthServices{ Services: iamidentity.UserAuthServices{
Basic: []string{s.Auth.Password}, Basic: []string{s.Auth.Password},
}, },
}, },
@@ -503,7 +516,7 @@ func (a *api) start() error {
users[s.Auth.Username] = user users[s.Auth.Username] = user
} }
policies = append(policies, iam.Policy{ policies = append(policies, iamaccess.Policy{
Name: s.Auth.Username, Name: s.Auth.Username,
Domain: "$none", Domain: "$none",
Resource: "fs:" + s.Mountpoint + "/**", Resource: "fs:" + s.Mountpoint + "/**",
@@ -513,7 +526,7 @@ func (a *api) start() error {
} }
if cfg.RTMP.Enable && len(cfg.RTMP.Token) == 0 { if cfg.RTMP.Enable && len(cfg.RTMP.Token) == 0 {
policies = append(policies, iam.Policy{ policies = append(policies, iamaccess.Policy{
Name: "$anon", Name: "$anon",
Domain: "$none", Domain: "$none",
Resource: "rtmp:/**", Resource: "rtmp:/**",
@@ -522,7 +535,7 @@ func (a *api) start() error {
} }
if cfg.SRT.Enable && len(cfg.SRT.Token) == 0 { if cfg.SRT.Enable && len(cfg.SRT.Token) == 0 {
policies = append(policies, iam.Policy{ policies = append(policies, iamaccess.Policy{
Name: "$anon", Name: "$anon",
Domain: "$none", Domain: "$none",
Resource: "srt:**", Resource: "srt:**",
@@ -737,7 +750,7 @@ func (a *api) start() error {
} }
template += "/{name}" template += "/{name}"
var identity iam.IdentityVerifier = nil var identity iamidentity.Verifier = nil
if len(config.Owner) == 0 { if len(config.Owner) == 0 {
identity = a.iam.GetDefaultVerifier() identity = a.iam.GetDefaultVerifier()
@@ -763,7 +776,7 @@ func (a *api) start() error {
template += ",mode:publish" template += ",mode:publish"
} }
var identity iam.IdentityVerifier = nil var identity iamidentity.Verifier = nil
if len(config.Owner) == 0 { if len(config.Owner) == 0 {
identity = a.iam.GetDefaultVerifier() identity = a.iam.GetDefaultVerifier()

View File

@@ -1,6 +1,9 @@
package api package api
import "github.com/datarhei/core/v16/iam" import (
"github.com/datarhei/core/v16/iam/access"
"github.com/datarhei/core/v16/iam/identity"
)
type IAMUser struct { type IAMUser struct {
Name string `json:"name"` Name string `json:"name"`
@@ -9,7 +12,7 @@ type IAMUser struct {
Policies []IAMPolicy `json:"policies"` Policies []IAMPolicy `json:"policies"`
} }
func (u *IAMUser) Marshal(user iam.User, policies []iam.Policy) { func (u *IAMUser) Marshal(user identity.User, policies []access.Policy) {
u.Name = user.Name u.Name = user.Name
u.Superuser = user.Superuser u.Superuser = user.Superuser
u.Auth = IAMUserAuth{ u.Auth = IAMUserAuth{
@@ -39,33 +42,33 @@ func (u *IAMUser) Marshal(user iam.User, policies []iam.Policy) {
} }
} }
func (u *IAMUser) Unmarshal() (iam.User, []iam.Policy) { func (u *IAMUser) Unmarshal() (identity.User, []access.Policy) {
iamuser := iam.User{ iamuser := identity.User{
Name: u.Name, Name: u.Name,
Superuser: u.Superuser, Superuser: u.Superuser,
Auth: iam.UserAuth{ Auth: identity.UserAuth{
API: iam.UserAuthAPI{ API: identity.UserAuthAPI{
Password: u.Auth.API.Password, Password: u.Auth.API.Password,
Auth0: iam.UserAuthAPIAuth0{ Auth0: identity.UserAuthAPIAuth0{
User: u.Auth.API.Auth0.User, User: u.Auth.API.Auth0.User,
Tenant: iam.Auth0Tenant{ Tenant: identity.Auth0Tenant{
Domain: u.Auth.API.Auth0.Tenant.Domain, Domain: u.Auth.API.Auth0.Tenant.Domain,
Audience: u.Auth.API.Auth0.Tenant.Audience, Audience: u.Auth.API.Auth0.Tenant.Audience,
ClientID: u.Auth.API.Auth0.Tenant.ClientID, ClientID: u.Auth.API.Auth0.Tenant.ClientID,
}, },
}, },
}, },
Services: iam.UserAuthServices{ Services: identity.UserAuthServices{
Basic: u.Auth.Services.Basic, Basic: u.Auth.Services.Basic,
Token: u.Auth.Services.Token, Token: u.Auth.Services.Token,
}, },
}, },
} }
iampolicies := []iam.Policy{} iampolicies := []access.Policy{}
for _, p := range u.Policies { for _, p := range u.Policies {
iampolicies = append(iampolicies, iam.Policy{ iampolicies = append(iampolicies, access.Policy{
Name: u.Name, Name: u.Name,
Domain: p.Domain, Domain: p.Domain,
Resource: p.Resource, Resource: p.Resource,

View File

@@ -6,6 +6,7 @@ import (
"github.com/datarhei/core/v16/http/api" "github.com/datarhei/core/v16/http/api"
"github.com/datarhei/core/v16/http/handler/util" "github.com/datarhei/core/v16/http/handler/util"
"github.com/datarhei/core/v16/iam" "github.com/datarhei/core/v16/iam"
"github.com/datarhei/core/v16/iam/identity"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
@@ -70,11 +71,6 @@ func (h *IAMHandler) AddUser(c echo.Context) error {
h.iam.AddPolicy(p.Name, p.Domain, p.Resource, p.Actions) h.iam.AddPolicy(p.Name, p.Domain, p.Resource, p.Actions)
} }
err = h.iam.SaveIdentities()
if err != nil {
return api.Err(http.StatusInternalServerError, "Internal server error", "%s", err)
}
return c.JSON(http.StatusOK, user) return c.JSON(http.StatusOK, user)
} }
@@ -116,11 +112,6 @@ func (h *IAMHandler) RemoveUser(c echo.Context) error {
return api.Err(http.StatusBadRequest, "Bad request", "%s", err) return api.Err(http.StatusBadRequest, "Bad request", "%s", err)
} }
err = h.iam.SaveIdentities()
if err != nil {
return api.Err(http.StatusInternalServerError, "Internal server error", "%s", err)
}
// Remove all policies of that user // Remove all policies of that user
h.iam.RemovePolicy(name, "", "", nil) h.iam.RemovePolicy(name, "", "", nil)
@@ -153,7 +144,7 @@ func (h *IAMHandler) UpdateUser(c echo.Context) error {
return api.Err(http.StatusForbidden, "Forbidden", "Not allowed to modify this user") return api.Err(http.StatusForbidden, "Forbidden", "Not allowed to modify this user")
} }
var iamuser iam.User var iamuser identity.User
var err error var err error
if name != "$anon" { if name != "$anon" {
@@ -162,7 +153,7 @@ func (h *IAMHandler) UpdateUser(c echo.Context) error {
return api.Err(http.StatusNotFound, "Not found", "%s", err) return api.Err(http.StatusNotFound, "Not found", "%s", err)
} }
} else { } else {
iamuser = iam.User{ iamuser = identity.User{
Name: "$anon", Name: "$anon",
} }
} }
@@ -205,11 +196,6 @@ func (h *IAMHandler) UpdateUser(c echo.Context) error {
h.iam.AddPolicy(p.Name, p.Domain, p.Resource, p.Actions) h.iam.AddPolicy(p.Name, p.Domain, p.Resource, p.Actions)
} }
err = h.iam.SaveIdentities()
if err != nil {
return api.Err(http.StatusInternalServerError, "Internal server error", "%s", err)
}
return c.JSON(http.StatusOK, user) return c.JSON(http.StatusOK, user)
} }
@@ -239,7 +225,7 @@ func (h *IAMHandler) UpdateUserPolicies(c echo.Context) error {
return api.Err(http.StatusForbidden, "Forbidden", "Not allowed to modify this user") return api.Err(http.StatusForbidden, "Forbidden", "Not allowed to modify this user")
} }
var iamuser iam.User var iamuser identity.User
var err error var err error
if name != "$anon" { if name != "$anon" {
@@ -248,7 +234,7 @@ func (h *IAMHandler) UpdateUserPolicies(c echo.Context) error {
return api.Err(http.StatusNotFound, "Not found", "%s", err) return api.Err(http.StatusNotFound, "Not found", "%s", err)
} }
} else { } else {
iamuser = iam.User{ iamuser = identity.User{
Name: "$anon", Name: "$anon",
} }
} }
@@ -300,7 +286,7 @@ func (h *IAMHandler) GetUser(c echo.Context) error {
return api.Err(http.StatusForbidden, "Forbidden", "Not allowed to access this user") return api.Err(http.StatusForbidden, "Forbidden", "Not allowed to access this user")
} }
var iamuser iam.User var iamuser identity.User
var err error var err error
if name != "$anon" { if name != "$anon" {
@@ -311,13 +297,13 @@ func (h *IAMHandler) GetUser(c echo.Context) error {
if !superuser && name != iamuser.Name { if !superuser && name != iamuser.Name {
if !h.iam.Enforce(ctxuser, domain, "iam:"+name, "write") { if !h.iam.Enforce(ctxuser, domain, "iam:"+name, "write") {
iamuser = iam.User{ iamuser = identity.User{
Name: iamuser.Name, Name: iamuser.Name,
} }
} }
} }
} else { } else {
iamuser = iam.User{ iamuser = identity.User{
Name: "$anon", Name: "$anon",
} }
} }

View File

@@ -10,6 +10,8 @@ import (
"github.com/datarhei/core/v16/http/api" "github.com/datarhei/core/v16/http/api"
"github.com/datarhei/core/v16/http/mock" "github.com/datarhei/core/v16/http/mock"
"github.com/datarhei/core/v16/iam" "github.com/datarhei/core/v16/iam"
"github.com/datarhei/core/v16/iam/access"
"github.com/datarhei/core/v16/iam/identity"
"github.com/datarhei/core/v16/io/fs" "github.com/datarhei/core/v16/io/fs"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
@@ -33,9 +35,20 @@ func getDummyRestreamHandler() (*RestreamHandler, error) {
return nil, fmt.Errorf("failed to create memory filesystem: %w", err) return nil, fmt.Errorf("failed to create memory filesystem: %w", err)
} }
policyAdapter, err := access.NewJSONAdapter(memfs, "./policy.json", nil)
if err != nil {
return nil, err
}
identityAdapter, err := identity.NewJSONAdapter(memfs, "./users.json", nil)
if err != nil {
return nil, err
}
iam, err := iam.NewIAM(iam.Config{ iam, err := iam.NewIAM(iam.Config{
FS: memfs, PolicyAdapter: policyAdapter,
Superuser: iam.User{ IdentityAdapter: identityAdapter,
Superuser: identity.User{
Name: "foobar", Name: "foobar",
}, },
JWTRealm: "", JWTRealm: "",

View File

@@ -44,6 +44,7 @@ import (
"github.com/datarhei/core/v16/http/api" "github.com/datarhei/core/v16/http/api"
"github.com/datarhei/core/v16/http/handler/util" "github.com/datarhei/core/v16/http/handler/util"
"github.com/datarhei/core/v16/iam" "github.com/datarhei/core/v16/iam"
iamidentity "github.com/datarhei/core/v16/iam/identity"
"github.com/datarhei/core/v16/log" "github.com/datarhei/core/v16/log"
jwtgo "github.com/golang-jwt/jwt/v4" jwtgo "github.com/golang-jwt/jwt/v4"
@@ -117,7 +118,7 @@ func NewWithConfig(config Config) echo.MiddlewareFunc {
isAPISuperuser = true isAPISuperuser = true
} }
var identity iam.IdentityVerifier = nil var identity iamidentity.Verifier = nil
var err error var err error
username := "$anon" username := "$anon"
@@ -231,7 +232,7 @@ var ErrAuthRequired = errors.New("unauthorized")
var ErrUnauthorized = errors.New("unauthorized") var ErrUnauthorized = errors.New("unauthorized")
var ErrBadRequest = errors.New("bad request") var ErrBadRequest = errors.New("bad request")
func (m *iammiddleware) findIdentityFromBasicAuth(c echo.Context) (iam.IdentityVerifier, error) { func (m *iammiddleware) findIdentityFromBasicAuth(c echo.Context) (iamidentity.Verifier, error) {
basic := "basic" basic := "basic"
auth := c.Request().Header.Get(echo.HeaderAuthorization) auth := c.Request().Header.Get(echo.HeaderAuthorization)
l := len(basic) l := len(basic)
@@ -290,7 +291,7 @@ func (m *iammiddleware) findIdentityFromBasicAuth(c echo.Context) (iam.IdentityV
return identity, nil return identity, nil
} }
func (m *iammiddleware) findIdentityFromJWT(c echo.Context) (iam.IdentityVerifier, error) { func (m *iammiddleware) findIdentityFromJWT(c echo.Context) (iamidentity.Verifier, error) {
// Look for an Auth header // Look for an Auth header
values := c.Request().Header.Values("Authorization") values := c.Request().Header.Values("Authorization")
prefix := "Bearer " prefix := "Bearer "
@@ -356,7 +357,7 @@ func (m *iammiddleware) findIdentityFromJWT(c echo.Context) (iam.IdentityVerifie
return identity, nil return identity, nil
} }
func (m *iammiddleware) findIdentityFromUserpass(c echo.Context) (iam.IdentityVerifier, error) { func (m *iammiddleware) findIdentityFromUserpass(c echo.Context) (iamidentity.Verifier, error) {
var login api.Login var login api.Login
if err := util.ShouldBindJSON(c, &login); err != nil { if err := util.ShouldBindJSON(c, &login); err != nil {
@@ -383,7 +384,7 @@ func (m *iammiddleware) findIdentityFromUserpass(c echo.Context) (iam.IdentityVe
return identity, nil return identity, nil
} }
func (m *iammiddleware) findIdentityFromAuth0(c echo.Context) (iam.IdentityVerifier, error) { func (m *iammiddleware) findIdentityFromAuth0(c echo.Context) (iamidentity.Verifier, error) {
// Look for an Auth header // Look for an Auth header
values := c.Request().Header.Values("Authorization") values := c.Request().Header.Values("Authorization")
prefix := "Bearer " prefix := "Bearer "

View File

@@ -14,6 +14,8 @@ import (
apihandler "github.com/datarhei/core/v16/http/handler/api" apihandler "github.com/datarhei/core/v16/http/handler/api"
"github.com/datarhei/core/v16/http/validator" "github.com/datarhei/core/v16/http/validator"
"github.com/datarhei/core/v16/iam" "github.com/datarhei/core/v16/iam"
iamaccess "github.com/datarhei/core/v16/iam/access"
iamidentity "github.com/datarhei/core/v16/iam/identity"
"github.com/datarhei/core/v16/io/fs" "github.com/datarhei/core/v16/io/fs"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
@@ -28,9 +30,20 @@ func getIAM() (iam.IAM, error) {
return nil, err return nil, err
} }
policyAdapter, err := iamaccess.NewJSONAdapter(dummyfs, "./policy.json", nil)
if err != nil {
return nil, err
}
identityAdapter, err := iamidentity.NewJSONAdapter(dummyfs, "./users.json", nil)
if err != nil {
return nil, err
}
i, err := iam.NewIAM(iam.Config{ i, err := iam.NewIAM(iam.Config{
FS: dummyfs, PolicyAdapter: policyAdapter,
Superuser: iam.User{ IdentityAdapter: identityAdapter,
Superuser: iamidentity.User{
Name: "admin", Name: "admin",
}, },
JWTRealm: "datarhei-core", JWTRealm: "datarhei-core",
@@ -41,13 +54,13 @@ func getIAM() (iam.IAM, error) {
return nil, err return nil, err
} }
i.CreateIdentity(iam.User{ i.CreateIdentity(iamidentity.User{
Name: "foobar", Name: "foobar",
Auth: iam.UserAuth{ Auth: iamidentity.UserAuth{
API: iam.UserAuthAPI{ API: iamidentity.UserAuthAPI{
Password: "secret", Password: "secret",
}, },
Services: iam.UserAuthServices{ Services: iamidentity.UserAuthServices{
Basic: []string{"secret"}, Basic: []string{"secret"},
}, },
}, },

View File

@@ -17,6 +17,8 @@ import (
"github.com/datarhei/core/v16/http/errorhandler" "github.com/datarhei/core/v16/http/errorhandler"
"github.com/datarhei/core/v16/http/validator" "github.com/datarhei/core/v16/http/validator"
"github.com/datarhei/core/v16/iam" "github.com/datarhei/core/v16/iam"
iamaccess "github.com/datarhei/core/v16/iam/access"
iamidentity "github.com/datarhei/core/v16/iam/identity"
"github.com/datarhei/core/v16/internal/testhelper" "github.com/datarhei/core/v16/internal/testhelper"
"github.com/datarhei/core/v16/io/fs" "github.com/datarhei/core/v16/io/fs"
"github.com/datarhei/core/v16/restream" "github.com/datarhei/core/v16/restream"
@@ -53,9 +55,20 @@ func DummyRestreamer(pathPrefix string) (restream.Restreamer, error) {
return nil, err return nil, err
} }
policyAdapter, err := iamaccess.NewJSONAdapter(memfs, "./policy.json", nil)
if err != nil {
return nil, err
}
identityAdapter, err := iamidentity.NewJSONAdapter(memfs, "./users.json", nil)
if err != nil {
return nil, err
}
iam, err := iam.NewIAM(iam.Config{ iam, err := iam.NewIAM(iam.Config{
FS: memfs, PolicyAdapter: policyAdapter,
Superuser: iam.User{ IdentityAdapter: identityAdapter,
Superuser: iamidentity.User{
Name: "foobar", Name: "foobar",
}, },
JWTRealm: "", JWTRealm: "",

View File

@@ -1,10 +1,8 @@
package iam package access
import ( import (
"fmt"
"strings" "strings"
"github.com/datarhei/core/v16/io/fs"
"github.com/datarhei/core/v16/log" "github.com/datarhei/core/v16/log"
"github.com/casbin/casbin/v2" "github.com/casbin/casbin/v2"
@@ -18,43 +16,40 @@ type Policy struct {
Actions []string Actions []string
} }
type AccessEnforcer interface { type Enforcer interface {
Enforce(name, domain, resource, action string) (bool, string) Enforce(name, domain, resource, action string) (bool, string)
HasDomain(name string) bool HasDomain(name string) bool
ListDomains() []string ListDomains() []string
} }
type AccessManager interface { type Manager interface {
AccessEnforcer Enforcer
HasPolicy(name, domain, resource string, actions []string) bool HasPolicy(name, domain, resource string, actions []string) bool
AddPolicy(name, domain, resource string, actions []string) bool AddPolicy(name, domain, resource string, actions []string) bool
RemovePolicy(name, domain, resource string, actions []string) bool RemovePolicy(name, domain, resource string, actions []string) bool
ListPolicies(name, domain, resource string, actions []string) []Policy ListPolicies(name, domain, resource string, actions []string) []Policy
ReloadPolicies() error
} }
type access struct { type access struct {
fs fs.Filesystem
logger log.Logger logger log.Logger
adapter *adapter adapter Adapter
model model.Model
enforcer *casbin.Enforcer enforcer *casbin.Enforcer
} }
type AccessConfig struct { type Config struct {
FS fs.Filesystem Adapter Adapter
Logger log.Logger Logger log.Logger
} }
func NewAccessManager(config AccessConfig) (AccessManager, error) { func New(config Config) (Manager, error) {
am := &access{ am := &access{
fs: config.FS, adapter: config.Adapter,
logger: config.Logger, logger: config.Logger,
}
if am.fs == nil {
return nil, fmt.Errorf("a filesystem has to be provided")
} }
if am.logger == nil { if am.logger == nil {
@@ -68,12 +63,7 @@ func NewAccessManager(config AccessConfig) (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, err := newAdapter(am.fs, "./policy.json", am.logger) e, err := casbin.NewEnforcer(m, am.adapter)
if err != nil {
return nil, err
}
e, err := casbin.NewEnforcer(m, a)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -82,7 +72,7 @@ func NewAccessManager(config AccessConfig) (AccessManager, error) {
e.AddFunction("ActionMatch", actionMatchFunc) e.AddFunction("ActionMatch", actionMatchFunc)
am.enforcer = e am.enforcer = e
am.adapter = a am.model = m
return am, nil return am, nil
} }
@@ -129,20 +119,18 @@ func (am *access) ListPolicies(name, domain, resource string, actions []string)
return policies return policies
} }
func (am *access) ReloadPolicies() error {
am.model.ClearPolicy()
return am.adapter.LoadPolicy(am.model)
}
func (am *access) HasDomain(name string) bool { func (am *access) HasDomain(name string) bool {
groups := am.adapter.getAllDomains() return am.adapter.HasDomain(name)
for _, g := range groups {
if g == name {
return true
}
}
return false
} }
func (am *access) ListDomains() []string { func (am *access) ListDomains() []string {
return am.adapter.getAllDomains() return am.adapter.AllDomains()
} }
func (am *access) Enforce(name, domain, resource, action string) (bool, string) { func (am *access) Enforce(name, domain, resource, action string) (bool, string) {

View File

@@ -1,4 +1,4 @@
package iam package access
import ( import (
"testing" "testing"
@@ -7,13 +7,22 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestAccessManager(t *testing.T) { func createAdapter() (Adapter, error) {
memfs, err := fs.NewMemFilesystemFromDir("./fixtures", fs.MemConfig{}) memfs, err := fs.NewMemFilesystemFromDir("./fixtures", fs.MemConfig{})
if err != nil {
return nil, err
}
return NewJSONAdapter(memfs, "./policy.json", nil)
}
func TestAccessManager(t *testing.T) {
adapter, err := createAdapter()
require.NoError(t, err) require.NoError(t, err)
am, err := NewAccessManager(AccessConfig{ am, err := New(Config{
FS: memfs, Adapter: adapter,
Logger: nil, Logger: nil,
}) })
require.NoError(t, err) require.NoError(t, err)

View File

@@ -1,4 +1,4 @@
package iam package access
import ( import (
"encoding/json" "encoding/json"
@@ -12,6 +12,7 @@ import (
"github.com/datarhei/core/v16/log" "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.
@@ -24,7 +25,14 @@ type adapter struct {
lock sync.Mutex lock sync.Mutex
} }
func newAdapter(fs fs.Filesystem, filePath string, logger log.Logger) (*adapter, error) { type Adapter interface {
persist.BatchAdapter
AllDomains() []string
HasDomain(string) bool
}
func NewJSONAdapter(fs fs.Filesystem, filePath string, logger log.Logger) (Adapter, error) {
a := &adapter{ a := &adapter{
fs: fs, fs: fs,
filePath: filePath, filePath: filePath,
@@ -72,6 +80,8 @@ func (a *adapter) loadPolicyFile(model model.Model) error {
return err return err
} }
model.ClearPolicy()
rule := [5]string{} rule := [5]string{}
for _, domain := range domains { for _, domain := range domains {
rule[0] = "p" rule[0] = "p"
@@ -511,7 +521,10 @@ func (a *adapter) RemoveFilteredPolicy(sec string, ptype string, fieldIndex int,
return fmt.Errorf("not implemented") return fmt.Errorf("not implemented")
} }
func (a *adapter) getAllDomains() []string { func (a *adapter) AllDomains() []string {
a.lock.Lock()
defer a.lock.Unlock()
names := []string{} names := []string{}
for _, domain := range a.domains { for _, domain := range a.domains {
@@ -525,6 +538,23 @@ func (a *adapter) getAllDomains() []string {
return names return names
} }
func (a *adapter) HasDomain(name string) bool {
a.lock.Lock()
defer a.lock.Unlock()
for _, domain := range a.domains {
if domain.Name[0] == '$' {
continue
}
if domain.Name == name {
return true
}
}
return false
}
type Domain struct { type Domain struct {
Name string `json:"name"` Name string `json:"name"`
Roles map[string][]Role `json:"roles"` Roles map[string][]Role `json:"roles"`

View File

@@ -1,4 +1,4 @@
package iam package access
import ( import (
"encoding/json" "encoding/json"
@@ -12,9 +12,12 @@ func TestAddPolicy(t *testing.T) {
memfs, err := fs.NewMemFilesystem(fs.MemConfig{}) memfs, err := fs.NewMemFilesystem(fs.MemConfig{})
require.NoError(t, err) require.NoError(t, err)
a, err := newAdapter(memfs, "/policy.json", nil) ai, err := NewJSONAdapter(memfs, "/policy.json", nil)
require.NoError(t, err) require.NoError(t, err)
a, ok := ai.(*adapter)
require.True(t, ok)
err = a.AddPolicy("p", "p", []string{"foobar", "group", "resource", "action"}) err = a.AddPolicy("p", "p", []string{"foobar", "group", "resource", "action"})
require.NoError(t, err) require.NoError(t, err)
@@ -53,9 +56,12 @@ func TestRemovePolicy(t *testing.T) {
memfs, err := fs.NewMemFilesystem(fs.MemConfig{}) memfs, err := fs.NewMemFilesystem(fs.MemConfig{})
require.NoError(t, err) require.NoError(t, err)
a, err := newAdapter(memfs, "/policy.json", nil) ai, err := NewJSONAdapter(memfs, "/policy.json", nil)
require.NoError(t, err) require.NoError(t, err)
a, ok := ai.(*adapter)
require.True(t, ok)
err = a.AddPolicies("p", "p", [][]string{ err = a.AddPolicies("p", "p", [][]string{
{"foobar1", "group", "resource1", "action1"}, {"foobar1", "group", "resource1", "action1"},
{"foobar2", "group", "resource2", "action2"}, {"foobar2", "group", "resource2", "action2"},

View File

@@ -1,4 +1,4 @@
package iam package access
import ( import (
"strings" "strings"

View File

@@ -1,7 +1,8 @@
package iam package iam
import ( import (
"github.com/datarhei/core/v16/io/fs" "github.com/datarhei/core/v16/iam/access"
"github.com/datarhei/core/v16/iam/identity"
"github.com/datarhei/core/v16/log" "github.com/datarhei/core/v16/log"
) )
@@ -18,21 +19,21 @@ type IAM interface {
HasPolicy(name, domain, resource string, actions []string) bool HasPolicy(name, domain, resource string, actions []string) bool
AddPolicy(name, domain, resource string, actions []string) bool AddPolicy(name, domain, resource string, actions []string) bool
RemovePolicy(name, domain, resource string, actions []string) bool RemovePolicy(name, domain, resource string, actions []string) bool
ListPolicies(name, domain, resource string, actions []string) []access.Policy
ListPolicies(name, domain, resource string, actions []string) []Policy ReloadPolicies() error
Validators() []string Validators() []string
CreateIdentity(u User) error CreateIdentity(u identity.User) error
GetIdentity(name string) (User, error) GetIdentity(name string) (identity.User, error)
UpdateIdentity(name string, u User) error UpdateIdentity(name string, u identity.User) error
DeleteIdentity(name string) error DeleteIdentity(name string) error
ListIdentities() []User ListIdentities() []identity.User
SaveIdentities() error ReloadIndentities() error
GetVerifier(name string) (IdentityVerifier, error) GetVerifier(name string) (identity.Verifier, error)
GetVerfierFromAuth0(name string) (IdentityVerifier, error) GetVerfierFromAuth0(name string) (identity.Verifier, error)
GetDefaultVerifier() IdentityVerifier GetDefaultVerifier() identity.Verifier
CreateJWT(name string) (string, string, error) CreateJWT(name string) (string, string, error)
@@ -40,23 +41,24 @@ type IAM interface {
} }
type iam struct { type iam struct {
im IdentityManager im identity.Manager
am AccessManager am access.Manager
logger log.Logger logger log.Logger
} }
type Config struct { type Config struct {
FS fs.Filesystem PolicyAdapter access.Adapter
Superuser User IdentityAdapter identity.Adapter
JWTRealm string Superuser identity.User
JWTSecret string JWTRealm string
Logger log.Logger JWTSecret string
Logger log.Logger
} }
func NewIAM(config Config) (IAM, error) { func NewIAM(config Config) (IAM, error) {
im, err := NewIdentityManager(IdentityConfig{ im, err := identity.New(identity.Config{
FS: config.FS, Adapter: config.IdentityAdapter,
Superuser: config.Superuser, Superuser: config.Superuser,
JWTRealm: config.JWTRealm, JWTRealm: config.JWTRealm,
JWTSecret: config.JWTSecret, JWTSecret: config.JWTSecret,
@@ -66,9 +68,9 @@ func NewIAM(config Config) (IAM, error) {
return nil, err return nil, err
} }
am, err := NewAccessManager(AccessConfig{ am, err := access.New(access.Config{
FS: config.FS, Adapter: config.PolicyAdapter,
Logger: config.Logger, Logger: config.Logger,
}) })
if err != nil { if err != nil {
return nil, err return nil, err
@@ -138,15 +140,15 @@ func (i *iam) Enforce(name, domain, resource, action string) bool {
return ok return ok
} }
func (i *iam) CreateIdentity(u User) error { func (i *iam) CreateIdentity(u identity.User) error {
return i.im.Create(u) return i.im.Create(u)
} }
func (i *iam) GetIdentity(name string) (User, error) { func (i *iam) GetIdentity(name string) (identity.User, error) {
return i.im.Get(name) return i.im.Get(name)
} }
func (i *iam) UpdateIdentity(name string, u User) error { func (i *iam) UpdateIdentity(name string, u identity.User) error {
return i.im.Update(name, u) return i.im.Update(name, u)
} }
@@ -154,23 +156,23 @@ func (i *iam) DeleteIdentity(name string) error {
return i.im.Delete(name) return i.im.Delete(name)
} }
func (i *iam) ListIdentities() []User { func (i *iam) ListIdentities() []identity.User {
return nil return nil
} }
func (i *iam) SaveIdentities() error { func (i *iam) ReloadIndentities() error {
return i.im.Save() return i.im.Reload()
} }
func (i *iam) GetVerifier(name string) (IdentityVerifier, error) { func (i *iam) GetVerifier(name string) (identity.Verifier, error) {
return i.im.GetVerifier(name) return i.im.GetVerifier(name)
} }
func (i *iam) GetVerfierFromAuth0(name string) (IdentityVerifier, error) { func (i *iam) GetVerfierFromAuth0(name string) (identity.Verifier, error) {
return i.im.GetVerifierFromAuth0(name) return i.im.GetVerifierFromAuth0(name)
} }
func (i *iam) GetDefaultVerifier() IdentityVerifier { func (i *iam) GetDefaultVerifier() identity.Verifier {
v, _ := i.im.GetDefaultVerifier() v, _ := i.im.GetDefaultVerifier()
return v return v
@@ -220,6 +222,10 @@ func (i *iam) RemovePolicy(name, domain, resource string, actions []string) bool
return i.am.RemovePolicy(name, domain, resource, actions) return i.am.RemovePolicy(name, domain, resource, actions)
} }
func (i *iam) ListPolicies(name, domain, resource string, actions []string) []Policy { func (i *iam) ListPolicies(name, domain, resource string, actions []string) []access.Policy {
return i.am.ListPolicies(name, domain, resource, actions) return i.am.ListPolicies(name, domain, resource, actions)
} }
func (i *iam) ReloadPolicies() error {
return i.am.ReloadPolicies()
}

89
iam/identity/adapter.go Normal file
View File

@@ -0,0 +1,89 @@
package identity
import (
"encoding/json"
"fmt"
"os"
"sync"
"github.com/datarhei/core/v16/io/fs"
"github.com/datarhei/core/v16/log"
)
type Adapter interface {
LoadIdentities() ([]User, error)
SaveIdentities(user []User) error
}
type fileAdapter struct {
fs fs.Filesystem
filePath string
logger log.Logger
lock sync.Mutex
}
func NewJSONAdapter(fs fs.Filesystem, filePath string, logger log.Logger) (Adapter, error) {
a := &fileAdapter{
fs: fs,
filePath: filePath,
logger: logger,
}
if a.fs == nil {
return nil, fmt.Errorf("a filesystem must be provided")
}
if a.filePath == "" {
return nil, fmt.Errorf("invalid file path, file path cannot be empty")
}
if a.logger == nil {
a.logger = log.New("")
}
return a, nil
}
func (a *fileAdapter) LoadIdentities() ([]User, error) {
a.lock.Lock()
defer a.lock.Unlock()
if _, err := a.fs.Stat(a.filePath); os.IsNotExist(err) {
return nil, nil
}
data, err := a.fs.ReadFile(a.filePath)
if err != nil {
return nil, err
}
users := []User{}
err = json.Unmarshal(data, &users)
if err != nil {
return nil, err
}
a.logger.Debug().WithField("path", a.filePath).Log("Identity file loaded")
return users, nil
}
func (a *fileAdapter) SaveIdentities(user []User) error {
a.lock.Lock()
defer a.lock.Unlock()
jsondata, err := json.MarshalIndent(user, "", " ")
if err != nil {
return err
}
_, _, err = a.fs.WriteFileSafe(a.filePath, jsondata)
if err != nil {
return err
}
a.logger.Debug().WithField("path", a.filePath).Log("Identity file save")
return nil
}

View File

@@ -1,15 +1,12 @@
package iam package identity
import ( import (
"encoding/json"
"fmt" "fmt"
"os"
"regexp" "regexp"
"sync" "sync"
"time" "time"
"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/log" "github.com/datarhei/core/v16/log"
"github.com/google/uuid" "github.com/google/uuid"
@@ -79,7 +76,7 @@ func (u *User) clone() User {
return user return user
} }
type IdentityVerifier interface { type Verifier interface {
Name() string Name() string
VerifyJWT(jwt string) (bool, error) VerifyJWT(jwt string) (bool, error)
@@ -369,21 +366,23 @@ func (i *identity) IsSuperuser() bool {
return i.user.Superuser return i.user.Superuser
} }
type IdentityManager interface { type Manager interface {
Create(identity User) error Create(identity User) error
Update(name string, identity User) error Update(name string, identity User) error
Delete(name string) error Delete(name string) error
Get(name string) (User, error) Get(name string) (User, error)
GetVerifier(name string) (IdentityVerifier, error) GetVerifier(name string) (Verifier, error)
GetVerifierFromAuth0(name string) (IdentityVerifier, error) GetVerifierFromAuth0(name string) (Verifier, error)
GetDefaultVerifier() (IdentityVerifier, error) GetDefaultVerifier() (Verifier, error)
Reload() error // Reload users from adapter
Save() error // Save users to adapter
List() []User // List all users
Validators() []string Validators() []string
CreateJWT(name string) (string, string, error) CreateJWT(name string) (string, string, error)
Save() error
Autosave(bool)
Close() Close()
} }
@@ -395,8 +394,7 @@ type identityManager struct {
auth0UserIdentityMap map[string]string auth0UserIdentityMap map[string]string
fs fs.Filesystem adapter Adapter
filePath string
autosave bool autosave bool
logger log.Logger logger log.Logger
@@ -406,21 +404,20 @@ type identityManager struct {
lock sync.RWMutex lock sync.RWMutex
} }
type IdentityConfig struct { type Config struct {
FS fs.Filesystem Adapter Adapter
Superuser User Superuser User
JWTRealm string JWTRealm string
JWTSecret string JWTSecret string
Logger log.Logger Logger log.Logger
} }
func NewIdentityManager(config IdentityConfig) (IdentityManager, error) { func New(config Config) (Manager, error) {
im := &identityManager{ im := &identityManager{
identities: map[string]*identity{}, identities: map[string]*identity{},
tenants: map[string]*auth0Tenant{}, tenants: map[string]*auth0Tenant{},
auth0UserIdentityMap: map[string]string{}, auth0UserIdentityMap: map[string]string{},
fs: config.FS, adapter: config.Adapter,
filePath: "./users.json",
jwtRealm: config.JWTRealm, jwtRealm: config.JWTRealm,
jwtSecret: []byte(config.JWTSecret), jwtSecret: []byte(config.JWTSecret),
logger: config.Logger, logger: config.Logger,
@@ -430,13 +427,8 @@ func NewIdentityManager(config IdentityConfig) (IdentityManager, error) {
im.logger = log.New("") im.logger = log.New("")
} }
if im.fs == nil { if im.adapter == nil {
return nil, fmt.Errorf("no filesystem provided") return nil, fmt.Errorf("no adapter provided")
}
err := im.load(im.filePath)
if err != nil {
return nil, err
} }
config.Superuser.Superuser = true config.Superuser.Superuser = true
@@ -446,7 +438,13 @@ func NewIdentityManager(config IdentityConfig) (IdentityManager, error) {
} }
im.root = identity im.root = identity
im.autosave = true
err = im.Reload()
if err != nil {
return nil, err
}
im.Save()
return im, nil return im, nil
} }
@@ -455,7 +453,7 @@ func (im *identityManager) Close() {
im.lock.Lock() im.lock.Lock()
defer im.lock.Unlock() defer im.lock.Unlock()
im.fs = nil im.adapter = nil
im.auth0UserIdentityMap = map[string]string{} im.auth0UserIdentityMap = map[string]string{}
im.identities = map[string]*identity{} im.identities = map[string]*identity{}
im.root = nil im.root = nil
@@ -467,6 +465,51 @@ func (im *identityManager) Close() {
im.tenants = map[string]*auth0Tenant{} im.tenants = map[string]*auth0Tenant{}
} }
func (im *identityManager) Reload() error {
users, err := im.adapter.LoadIdentities()
if err != nil {
return fmt.Errorf("load users from adapter: %w", err)
}
im.lock.Lock()
defer im.lock.Unlock()
im.autosave = false
defer func() {
im.autosave = true
}()
names := []string{}
for name := range im.identities {
names = append(names, name)
}
for _, name := range names {
im.delete(name)
}
for _, u := range users {
if im.root != nil && u.Name == im.root.user.Name {
continue
}
_, ok := im.identities[u.Name]
if ok {
continue
}
identity, err := im.create(u)
if err != nil {
continue
}
im.identities[identity.user.Name] = identity
}
return nil
}
func (im *identityManager) Create(u User) error { func (im *identityManager) Create(u User) error {
if err := u.validate(); err != nil { if err := u.validate(); err != nil {
return err return err
@@ -492,7 +535,7 @@ func (im *identityManager) Create(u User) error {
im.identities[identity.user.Name] = identity im.identities[identity.user.Name] = identity
if im.autosave { if im.autosave {
im.save(im.filePath) im.save()
} }
return nil return nil
@@ -580,7 +623,7 @@ func (im *identityManager) Update(name string, u User) error {
}).Log("Identity updated") }).Log("Identity updated")
if im.autosave { if im.autosave {
im.save(im.filePath) im.save()
} }
return nil return nil
@@ -590,7 +633,12 @@ func (im *identityManager) Delete(name string) error {
im.lock.Lock() im.lock.Lock()
defer im.lock.Unlock() defer im.lock.Unlock()
return im.delete(name) err := im.delete(name)
if err != nil {
return err
}
return nil
} }
func (im *identityManager) delete(name string) error { func (im *identityManager) delete(name string) error {
@@ -611,7 +659,7 @@ func (im *identityManager) delete(name string) error {
if len(identity.user.Auth.API.Auth0.User) == 0 { if len(identity.user.Auth.API.Auth0.User) == 0 {
if im.autosave { if im.autosave {
im.save(im.filePath) im.save()
} }
return nil return nil
@@ -633,7 +681,7 @@ func (im *identityManager) delete(name string) error {
delete(im.tenants, identity.user.Auth.API.Auth0.Tenant.key()) delete(im.tenants, identity.user.Auth.API.Auth0.Tenant.key())
if im.autosave { if im.autosave {
im.save(im.filePath) im.save()
} }
return nil return nil
@@ -657,7 +705,9 @@ func (im *identityManager) delete(name string) error {
} }
if im.autosave { if im.autosave {
im.save(im.filePath) if err := im.save(); err != nil {
return err
}
} }
return nil return nil
@@ -696,14 +746,14 @@ func (im *identityManager) Get(name string) (User, error) {
return user, nil return user, nil
} }
func (im *identityManager) GetVerifier(name string) (IdentityVerifier, error) { func (im *identityManager) GetVerifier(name string) (Verifier, error) {
im.lock.RLock() im.lock.RLock()
defer im.lock.RUnlock() defer im.lock.RUnlock()
return im.getIdentity(name) return im.getIdentity(name)
} }
func (im *identityManager) GetVerifierFromAuth0(name string) (IdentityVerifier, error) { func (im *identityManager) GetVerifierFromAuth0(name string) (Verifier, error) {
im.lock.RLock() im.lock.RLock()
defer im.lock.RUnlock() defer im.lock.RUnlock()
@@ -715,65 +765,38 @@ func (im *identityManager) GetVerifierFromAuth0(name string) (IdentityVerifier,
return im.getIdentity(name) return im.getIdentity(name)
} }
func (im *identityManager) GetDefaultVerifier() (IdentityVerifier, error) { func (im *identityManager) GetDefaultVerifier() (Verifier, error) {
return im.root, nil return im.root, nil
} }
func (im *identityManager) load(filePath string) error { func (im *identityManager) List() []User {
if _, err := im.fs.Stat(filePath); os.IsNotExist(err) { im.lock.RLock()
return nil defer im.lock.RUnlock()
}
data, err := im.fs.ReadFile(filePath)
if err != nil {
return err
}
users := []User{} users := []User{}
err = json.Unmarshal(data, &users) for _, identity := range im.identities {
if err != nil { users = append(users, identity.user.clone())
return err
} }
for _, u := range users { return users
err = im.Create(u)
if err != nil {
return err
}
}
return nil
} }
func (im *identityManager) Save() error { func (im *identityManager) Save() error {
im.lock.RLock() im.lock.RLock()
defer im.lock.RUnlock() defer im.lock.RUnlock()
return im.save(im.filePath) return im.save()
} }
func (im *identityManager) save(filePath string) error { func (im *identityManager) save() error {
if filePath == "" {
return fmt.Errorf("invalid file path, file path cannot be empty")
}
users := []User{} users := []User{}
for _, u := range im.identities { for _, u := range im.identities {
users = append(users, u.user) users = append(users, u.user)
} }
jsondata, err := json.MarshalIndent(users, "", " ") return im.adapter.SaveIdentities(users)
if err != nil {
return err
}
_, _, err = im.fs.WriteFileSafe(filePath, jsondata)
im.logger.Debug().WithField("path", filePath).Log("Identity file save")
return err
} }
func (im *identityManager) Autosave(auto bool) { func (im *identityManager) Autosave(auto bool) {

View File

@@ -1,4 +1,4 @@
package iam package identity
import ( import (
"testing" "testing"
@@ -7,6 +7,15 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func createAdapter() (Adapter, error) {
dummyfs, err := fs.NewMemFilesystem(fs.MemConfig{})
if err != nil {
return nil, err
}
return NewJSONAdapter(dummyfs, "./users.json", nil)
}
func TestUserName(t *testing.T) { func TestUserName(t *testing.T) {
user := User{} user := User{}
@@ -39,11 +48,11 @@ func TestIdentity(t *testing.T) {
identity.user.Superuser = true identity.user.Superuser = true
require.True(t, identity.IsSuperuser()) require.True(t, identity.IsSuperuser())
dummyfs, err := fs.NewMemFilesystem(fs.MemConfig{}) adapter, err := createAdapter()
require.NoError(t, err) require.NoError(t, err)
im, err := NewIdentityManager(IdentityConfig{ im, err := New(Config{
FS: dummyfs, Adapter: adapter,
Superuser: User{Name: "foobar"}, Superuser: User{Name: "foobar"},
JWTRealm: "test-realm", JWTRealm: "test-realm",
JWTSecret: "abc123", JWTSecret: "abc123",
@@ -58,11 +67,11 @@ func TestIdentity(t *testing.T) {
} }
func TestDefaultIdentity(t *testing.T) { func TestDefaultIdentity(t *testing.T) {
dummyfs, err := fs.NewMemFilesystem(fs.MemConfig{}) adapter, err := createAdapter()
require.NoError(t, err) require.NoError(t, err)
im, err := NewIdentityManager(IdentityConfig{ im, err := New(Config{
FS: dummyfs, Adapter: adapter,
Superuser: User{Name: "foobar"}, Superuser: User{Name: "foobar"},
JWTRealm: "test-realm", JWTRealm: "test-realm",
JWTSecret: "abc123", JWTSecret: "abc123",
@@ -186,11 +195,11 @@ func TestIdentityServiceTokenAuth(t *testing.T) {
} }
func TestJWT(t *testing.T) { func TestJWT(t *testing.T) {
dummyfs, err := fs.NewMemFilesystem(fs.MemConfig{}) adapter, err := createAdapter()
require.NoError(t, err) require.NoError(t, err)
im, err := NewIdentityManager(IdentityConfig{ im, err := New(Config{
FS: dummyfs, Adapter: adapter,
Superuser: User{Name: "foobar"}, Superuser: User{Name: "foobar"},
JWTRealm: "test-realm", JWTRealm: "test-realm",
JWTSecret: "abc123", JWTSecret: "abc123",
@@ -239,11 +248,11 @@ func TestJWT(t *testing.T) {
} }
func TestCreateUser(t *testing.T) { func TestCreateUser(t *testing.T) {
dummyfs, err := fs.NewMemFilesystem(fs.MemConfig{}) adapter, err := createAdapter()
require.NoError(t, err) require.NoError(t, err)
im, err := NewIdentityManager(IdentityConfig{ im, err := New(Config{
FS: dummyfs, Adapter: adapter,
Superuser: User{Name: "foobar"}, Superuser: User{Name: "foobar"},
JWTRealm: "test-realm", JWTRealm: "test-realm",
JWTSecret: "abc123", JWTSecret: "abc123",
@@ -263,11 +272,11 @@ func TestCreateUser(t *testing.T) {
} }
func TestCreateUserAuth0(t *testing.T) { func TestCreateUserAuth0(t *testing.T) {
dummyfs, err := fs.NewMemFilesystem(fs.MemConfig{}) adapter, err := createAdapter()
require.NoError(t, err) require.NoError(t, err)
im, err := NewIdentityManager(IdentityConfig{ im, err := New(Config{
FS: dummyfs, Adapter: adapter,
Superuser: User{Name: "foobar"}, Superuser: User{Name: "foobar"},
JWTRealm: "test-realm", JWTRealm: "test-realm",
JWTSecret: "abc123", JWTSecret: "abc123",
@@ -383,11 +392,13 @@ func TestCreateUserAuth0(t *testing.T) {
} }
func TestLoadAndSave(t *testing.T) { func TestLoadAndSave(t *testing.T) {
dummyfs, err := fs.NewMemFilesystem(fs.MemConfig{}) adptr, err := createAdapter()
require.NoError(t, err) require.NoError(t, err)
im, err := NewIdentityManager(IdentityConfig{ dummyfs := adptr.(*fileAdapter).fs
FS: dummyfs,
im, err := New(Config{
Adapter: adptr,
Superuser: User{Name: "foobar"}, Superuser: User{Name: "foobar"},
JWTRealm: "test-realm", JWTRealm: "test-realm",
JWTSecret: "abc123", JWTSecret: "abc123",
@@ -416,8 +427,8 @@ func TestLoadAndSave(t *testing.T) {
err = im.Save() err = im.Save()
require.NoError(t, err) require.NoError(t, err)
im, err = NewIdentityManager(IdentityConfig{ im, err = New(Config{
FS: dummyfs, Adapter: adptr,
Superuser: User{Name: "foobar"}, Superuser: User{Name: "foobar"},
JWTRealm: "test-realm", JWTRealm: "test-realm",
JWTSecret: "abc123", JWTSecret: "abc123",
@@ -432,11 +443,11 @@ func TestLoadAndSave(t *testing.T) {
} }
func TestUpdateUser(t *testing.T) { func TestUpdateUser(t *testing.T) {
dummyfs, err := fs.NewMemFilesystem(fs.MemConfig{}) adapter, err := createAdapter()
require.NoError(t, err) require.NoError(t, err)
im, err := NewIdentityManager(IdentityConfig{ im, err := New(Config{
FS: dummyfs, Adapter: adapter,
Superuser: User{Name: "foobar"}, Superuser: User{Name: "foobar"},
JWTRealm: "test-realm", JWTRealm: "test-realm",
JWTSecret: "abc123", JWTSecret: "abc123",
@@ -480,11 +491,11 @@ func TestUpdateUser(t *testing.T) {
} }
func TestUpdateUserAuth0(t *testing.T) { func TestUpdateUserAuth0(t *testing.T) {
dummyfs, err := fs.NewMemFilesystem(fs.MemConfig{}) adapter, err := createAdapter()
require.NoError(t, err) require.NoError(t, err)
im, err := NewIdentityManager(IdentityConfig{ im, err := New(Config{
FS: dummyfs, Adapter: adapter,
Superuser: User{Name: "foobar"}, Superuser: User{Name: "foobar"},
JWTRealm: "test-realm", JWTRealm: "test-realm",
JWTSecret: "abc123", JWTSecret: "abc123",
@@ -537,11 +548,11 @@ func TestUpdateUserAuth0(t *testing.T) {
} }
func TestRemoveUser(t *testing.T) { func TestRemoveUser(t *testing.T) {
dummyfs, err := fs.NewMemFilesystem(fs.MemConfig{}) adapter, err := createAdapter()
require.NoError(t, err) require.NoError(t, err)
im, err := NewIdentityManager(IdentityConfig{ im, err := New(Config{
FS: dummyfs, Adapter: adapter,
Superuser: User{Name: "foobar"}, Superuser: User{Name: "foobar"},
JWTRealm: "test-realm", JWTRealm: "test-realm",
JWTSecret: "abc123", JWTSecret: "abc123",
@@ -633,11 +644,11 @@ func TestRemoveUser(t *testing.T) {
} }
func TestRemoveUserAuth0(t *testing.T) { func TestRemoveUserAuth0(t *testing.T) {
dummyfs, err := fs.NewMemFilesystem(fs.MemConfig{}) adapter, err := createAdapter()
require.NoError(t, err) require.NoError(t, err)
im, err := NewIdentityManager(IdentityConfig{ im, err := New(Config{
FS: dummyfs, Adapter: adapter,
Superuser: User{Name: "foobar"}, Superuser: User{Name: "foobar"},
JWTRealm: "test-realm", JWTRealm: "test-realm",
JWTSecret: "abc123", JWTSecret: "abc123",
@@ -716,11 +727,13 @@ func TestRemoveUserAuth0(t *testing.T) {
} }
func TestAutosave(t *testing.T) { func TestAutosave(t *testing.T) {
dummyfs, err := fs.NewMemFilesystem(fs.MemConfig{}) adptr, err := createAdapter()
require.NoError(t, err) require.NoError(t, err)
im, err := NewIdentityManager(IdentityConfig{ dummyfs := adptr.(*fileAdapter).fs
FS: dummyfs,
im, err := New(Config{
Adapter: adptr,
Superuser: User{Name: "foobar"}, Superuser: User{Name: "foobar"},
JWTRealm: "test-realm", JWTRealm: "test-realm",
JWTSecret: "abc123", JWTSecret: "abc123",
@@ -739,8 +752,6 @@ func TestAutosave(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, []byte("[]"), data) require.Equal(t, []byte("[]"), data)
im.Autosave(true)
err = im.Create(User{Name: "foobaz"}) err = im.Create(User{Name: "foobaz"})
require.NoError(t, err) require.NoError(t, err)

View File

@@ -7,6 +7,8 @@ import (
"github.com/datarhei/core/v16/ffmpeg" "github.com/datarhei/core/v16/ffmpeg"
"github.com/datarhei/core/v16/iam" "github.com/datarhei/core/v16/iam"
iamaccess "github.com/datarhei/core/v16/iam/access"
iamidentity "github.com/datarhei/core/v16/iam/identity"
"github.com/datarhei/core/v16/internal/testhelper" "github.com/datarhei/core/v16/internal/testhelper"
"github.com/datarhei/core/v16/io/fs" "github.com/datarhei/core/v16/io/fs"
"github.com/datarhei/core/v16/net" "github.com/datarhei/core/v16/net"
@@ -39,9 +41,20 @@ func getDummyRestreamer(portrange net.Portranger, validatorIn, validatorOut ffmp
return nil, err return nil, err
} }
policyAdapter, err := iamaccess.NewJSONAdapter(dummyfs, "./policy.json", nil)
if err != nil {
return nil, err
}
identityAdapter, err := iamidentity.NewJSONAdapter(dummyfs, "./users.json", nil)
if err != nil {
return nil, err
}
iam, err := iam.NewIAM(iam.Config{ iam, err := iam.NewIAM(iam.Config{
FS: dummyfs, PolicyAdapter: policyAdapter,
Superuser: iam.User{ IdentityAdapter: identityAdapter,
Superuser: iamidentity.User{
Name: "foobar", Name: "foobar",
}, },
JWTRealm: "", JWTRealm: "",

View File

@@ -5,7 +5,7 @@ import (
"fmt" "fmt"
"net/url" "net/url"
"github.com/datarhei/core/v16/iam" iamidentity "github.com/datarhei/core/v16/iam/identity"
"github.com/datarhei/core/v16/rtmp" "github.com/datarhei/core/v16/rtmp"
srturl "github.com/datarhei/core/v16/srt/url" srturl "github.com/datarhei/core/v16/srt/url"
) )
@@ -25,7 +25,7 @@ type Config struct {
// to a new identity, i.e. adjusting the credentials to the given identity. // to a new identity, i.e. adjusting the credentials to the given identity.
type Rewriter interface { type Rewriter interface {
RewriteAddress(address string, identity iam.IdentityVerifier, mode Access) string RewriteAddress(address string, identity iamidentity.Verifier, mode Access) string
} }
type rewrite struct { type rewrite struct {
@@ -44,7 +44,7 @@ func New(config Config) (Rewriter, error) {
return r, nil return r, nil
} }
func (g *rewrite) RewriteAddress(address string, identity iam.IdentityVerifier, mode Access) string { func (g *rewrite) RewriteAddress(address string, identity iamidentity.Verifier, mode Access) string {
u, err := url.Parse(address) u, err := url.Parse(address)
if err != nil { if err != nil {
return address return address
@@ -104,7 +104,7 @@ func (g *rewrite) isLocal(u *url.URL) bool {
return host == base.Host return host == base.Host
} }
func (g *rewrite) httpURL(u *url.URL, mode Access, identity iam.IdentityVerifier) string { func (g *rewrite) httpURL(u *url.URL, mode Access, identity iamidentity.Verifier) string {
password := identity.GetServiceBasicAuth() password := identity.GetServiceBasicAuth()
if len(password) == 0 { if len(password) == 0 {
@@ -116,7 +116,7 @@ func (g *rewrite) httpURL(u *url.URL, mode Access, identity iam.IdentityVerifier
return u.String() return u.String()
} }
func (g *rewrite) rtmpURL(u *url.URL, mode Access, identity iam.IdentityVerifier) string { func (g *rewrite) rtmpURL(u *url.URL, mode Access, identity iamidentity.Verifier) string {
token := identity.GetServiceToken() token := identity.GetServiceToken()
// Remove the existing token from the path // Remove the existing token from the path
@@ -131,7 +131,7 @@ func (g *rewrite) rtmpURL(u *url.URL, mode Access, identity iam.IdentityVerifier
return u.String() return u.String()
} }
func (g *rewrite) srtURL(u *url.URL, mode Access, identity iam.IdentityVerifier) string { func (g *rewrite) srtURL(u *url.URL, mode Access, identity iamidentity.Verifier) string {
token := identity.GetServiceToken() token := identity.GetServiceToken()
q := u.Query() q := u.Query()

View File

@@ -4,21 +4,26 @@ import (
"net/url" "net/url"
"testing" "testing"
"github.com/datarhei/core/v16/iam" iamidentity "github.com/datarhei/core/v16/iam/identity"
"github.com/datarhei/core/v16/io/fs" "github.com/datarhei/core/v16/io/fs"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func getIdentityManager(enableBasic bool) iam.IdentityManager { func getIdentityManager(enableBasic bool) (iamidentity.Manager, error) {
dummyfs, _ := fs.NewMemFilesystem(fs.MemConfig{}) dummyfs, _ := fs.NewMemFilesystem(fs.MemConfig{})
superuser := iam.User{ adapter, err := iamidentity.NewJSONAdapter(dummyfs, "./users.json", nil)
if err != nil {
return nil, err
}
superuser := iamidentity.User{
Name: "foobar", Name: "foobar",
Superuser: false, Superuser: false,
Auth: iam.UserAuth{ Auth: iamidentity.UserAuth{
API: iam.UserAuthAPI{}, API: iamidentity.UserAuthAPI{},
Services: iam.UserAuthServices{ Services: iamidentity.UserAuthServices{
Token: []string{"servicetoken"}, Token: []string{"servicetoken"},
}, },
}, },
@@ -28,19 +33,20 @@ func getIdentityManager(enableBasic bool) iam.IdentityManager {
superuser.Auth.Services.Basic = []string{"basicauthpassword"} superuser.Auth.Services.Basic = []string{"basicauthpassword"}
} }
im, _ := iam.NewIdentityManager(iam.IdentityConfig{ im, err := iamidentity.New(iamidentity.Config{
FS: dummyfs, Adapter: adapter,
Superuser: superuser, Superuser: superuser,
JWTRealm: "", JWTRealm: "",
JWTSecret: "", JWTSecret: "",
Logger: nil, Logger: nil,
}) })
return im return im, err
} }
func TestRewriteHTTP(t *testing.T) { func TestRewriteHTTP(t *testing.T) {
im := getIdentityManager(false) im, err := getIdentityManager(false)
require.NoError(t, err)
rewrite, err := New(Config{ rewrite, err := New(Config{
HTTPBase: "http://localhost:8080/", HTTPBase: "http://localhost:8080/",
@@ -70,7 +76,8 @@ func TestRewriteHTTP(t *testing.T) {
} }
func TestRewriteHTTPPassword(t *testing.T) { func TestRewriteHTTPPassword(t *testing.T) {
im := getIdentityManager(true) im, err := getIdentityManager(true)
require.NoError(t, err)
rewrite, err := New(Config{ rewrite, err := New(Config{
HTTPBase: "http://localhost:8080/", HTTPBase: "http://localhost:8080/",
@@ -100,7 +107,8 @@ func TestRewriteHTTPPassword(t *testing.T) {
} }
func TestRewriteRTMP(t *testing.T) { func TestRewriteRTMP(t *testing.T) {
im := getIdentityManager(false) im, err := getIdentityManager(false)
require.NoError(t, err)
rewrite, err := New(Config{ rewrite, err := New(Config{
RTMPBase: "rtmp://localhost:1935/live", RTMPBase: "rtmp://localhost:1935/live",
@@ -128,7 +136,8 @@ func TestRewriteRTMP(t *testing.T) {
} }
func TestRewriteSRT(t *testing.T) { func TestRewriteSRT(t *testing.T) {
im := getIdentityManager(false) im, err := getIdentityManager(false)
require.NoError(t, err)
rewrite, err := New(Config{ rewrite, err := New(Config{
SRTBase: "srt://localhost:6000/", SRTBase: "srt://localhost:6000/",

View File

@@ -13,6 +13,7 @@ import (
"github.com/datarhei/core/v16/cluster/proxy" "github.com/datarhei/core/v16/cluster/proxy"
"github.com/datarhei/core/v16/iam" "github.com/datarhei/core/v16/iam"
iamidentity "github.com/datarhei/core/v16/iam/identity"
"github.com/datarhei/core/v16/log" "github.com/datarhei/core/v16/log"
"github.com/datarhei/core/v16/session" "github.com/datarhei/core/v16/session"
@@ -467,7 +468,7 @@ func (s *server) findIdentityFromStreamKey(key string) (string, error) {
return "$anon", nil return "$anon", nil
} }
var identity iam.IdentityVerifier var identity iamidentity.Verifier
var err error var err error
var token string var token string

View File

@@ -11,6 +11,7 @@ import (
"github.com/datarhei/core/v16/cluster/proxy" "github.com/datarhei/core/v16/cluster/proxy"
"github.com/datarhei/core/v16/iam" "github.com/datarhei/core/v16/iam"
iamidentity "github.com/datarhei/core/v16/iam/identity"
"github.com/datarhei/core/v16/log" "github.com/datarhei/core/v16/log"
"github.com/datarhei/core/v16/session" "github.com/datarhei/core/v16/session"
"github.com/datarhei/core/v16/srt/url" "github.com/datarhei/core/v16/srt/url"
@@ -477,7 +478,7 @@ func (s *server) findIdentityFromToken(key string) (string, error) {
return "$anon", nil return "$anon", nil
} }
var identity iam.IdentityVerifier var identity iamidentity.Verifier
var err error var err error
var token string var token string