WIP: designing interfaces, detecting identity, enforcing policies

This commit is contained in:
Ingo Oppermann 2023-02-03 17:43:06 +01:00
parent e9caa1b033
commit 8f1ff2d1a2
No known key found for this signature in database
GPG Key ID: 2AB32426E9DD229E
10 changed files with 605 additions and 116 deletions

View File

@ -25,6 +25,7 @@ import (
httpfs "github.com/datarhei/core/v16/http/fs"
"github.com/datarhei/core/v16/http/jwt"
"github.com/datarhei/core/v16/http/router"
"github.com/datarhei/core/v16/iam"
"github.com/datarhei/core/v16/io/fs"
"github.com/datarhei/core/v16/log"
"github.com/datarhei/core/v16/math/rand"
@ -83,6 +84,7 @@ type api struct {
httpjwt jwt.JWT
update update.Checker
replacer replace.Replacer
iam iam.IAM
errorChan chan error
@ -381,6 +383,13 @@ func (a *api) start() error {
a.sessions = sessions
}
iam, err := iam.NewIAM()
if err != nil {
return fmt.Errorf("iam: %w", err)
}
a.iam = iam
diskfs, err := fs.NewRootedDiskFilesystem(fs.RootedDiskConfig{
Root: cfg.Storage.Disk.Dir,
Logger: a.log.logger.core.WithComponent("DiskFS"),
@ -874,6 +883,7 @@ func (a *api) start() error {
Token: cfg.RTMP.Token,
Logger: a.log.logger.rtmp,
Collector: a.sessions.Collector("rtmp"),
IAM: a.iam,
}
if cfg.RTMP.EnableTLS {
@ -902,6 +912,7 @@ func (a *api) start() error {
Token: cfg.SRT.Token,
Logger: a.log.logger.core.WithComponent("SRT").WithField("address", cfg.SRT.Address),
Collector: a.sessions.Collector("srt"),
IAM: a.iam,
}
if cfg.SRT.Log.Enable {
@ -1012,6 +1023,7 @@ func (a *api) start() error {
Sessions: a.sessions,
Router: router,
ReadOnly: cfg.API.ReadOnly,
IAM: a.iam,
}
mainserverhandler, err := http.NewServer(serverConfig)

View File

@ -92,7 +92,19 @@ func New(config Config) (JWT, error) {
AuthScheme: "Bearer",
Claims: jwtgo.MapClaims{},
ErrorHandlerWithContext: j.ErrorHandler,
ParseTokenFunc: j.parseToken("access"),
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)
},
ParseTokenFunc: j.parseToken("access"),
}
j.refreshConfig = middleware.JWTConfig{

View File

@ -6,7 +6,7 @@ import (
"github.com/datarhei/core/v16/http/api"
"github.com/datarhei/core/v16/http/handler/util"
"github.com/datarhei/core/v16/http/jwt/jwks"
"github.com/datarhei/core/v16/iam"
jwtgo "github.com/golang-jwt/jwt/v4"
"github.com/labstack/echo/v4"
@ -24,14 +24,12 @@ type Validator interface {
}
type localValidator struct {
username string
password string
iam iam.IAM
}
func NewLocalValidator(username, password string) (Validator, error) {
func NewLocalValidator(iam iam.IAM) (Validator, error) {
v := &localValidator{
username: username,
password: password,
iam: iam,
}
return v, nil
@ -48,46 +46,34 @@ func (v *localValidator) Validate(c echo.Context) (bool, string, error) {
return false, "", nil
}
if login.Username != v.username || login.Password != v.password {
identity := v.iam.GetIdentity(login.Username)
if identity == nil {
return true, "", fmt.Errorf("invalid username or password")
}
return true, v.username, nil
if !identity.VerifyAPIPassword(login.Password) {
return true, "", fmt.Errorf("invalid username or password")
}
return true, login.Username, nil
}
func (v *localValidator) Cancel() {}
type auth0Validator struct {
domain string
issuer string
audience string
clientID string
users []string
certs jwks.JWKS
iam iam.IAM
}
func NewAuth0Validator(domain, audience, clientID string, users []string) (Validator, error) {
func NewAuth0Validator(iam iam.IAM) (Validator, error) {
v := &auth0Validator{
domain: domain,
issuer: "https://" + domain + "/",
audience: audience,
clientID: clientID,
users: users,
iam: iam,
}
url := v.issuer + ".well-known/jwks.json"
certs, err := jwks.NewFromURL(url, jwks.Config{})
if err != nil {
return nil, err
}
v.certs = certs
return v, nil
}
func (v auth0Validator) String() string {
return fmt.Sprintf("auth0 domain=%s audience=%s clientid=%s", v.domain, v.audience, v.clientID)
return fmt.Sprintf("auth0 domain=%s audience=%s clientid=%s", "", "", "")
}
func (v *auth0Validator) Validate(c echo.Context) (bool, string, error) {
@ -116,26 +102,6 @@ func (v *auth0Validator) Validate(c echo.Context) (bool, string, error) {
return false, "", nil
}
var issuer string
if claims, ok := token.Claims.(jwtgo.MapClaims); ok {
if iss, ok := claims["iss"]; ok {
issuer = iss.(string)
}
}
if issuer != v.issuer {
return false, "", nil
}
token, err = jwtgo.Parse(auth, v.keyFunc)
if err != nil {
return true, "", err
}
if !token.Valid {
return true, "", fmt.Errorf("invalid token")
}
var subject string
if claims, ok := token.Claims.(jwtgo.MapClaims); ok {
if sub, ok := claims["sub"]; ok {
@ -143,72 +109,16 @@ func (v *auth0Validator) Validate(c echo.Context) (bool, string, error) {
}
}
identity := v.iam.GetIdentityByAuth0(subject)
if identity == nil {
return true, "", fmt.Errorf("invalid token")
}
if !identity.VerifyAPIAuth0(auth) {
return true, "", fmt.Errorf("invalid token")
}
return true, subject, nil
}
func (v *auth0Validator) keyFunc(token *jwtgo.Token) (interface{}, error) {
// Verify 'aud' claim
checkAud := token.Claims.(jwtgo.MapClaims).VerifyAudience(v.audience, false)
if !checkAud {
return nil, fmt.Errorf("invalid audience")
}
// Verify 'iss' claim
checkIss := token.Claims.(jwtgo.MapClaims).VerifyIssuer(v.issuer, false)
if !checkIss {
return nil, fmt.Errorf("invalid issuer")
}
// Verify 'sub' claim
if _, ok := token.Claims.(jwtgo.MapClaims)["sub"]; !ok {
return nil, fmt.Errorf("sub claim is required")
}
sub := token.Claims.(jwtgo.MapClaims)["sub"].(string)
found := false
for _, u := range v.users {
if sub == u {
found = true
break
}
}
if !found {
return nil, fmt.Errorf("user not allowed")
}
// find the key
if _, ok := token.Header["kid"]; !ok {
return nil, fmt.Errorf("kid not found")
}
kid := token.Header["kid"].(string)
key, err := v.certs.Key(kid)
if err != nil {
return nil, fmt.Errorf("no cert for kid found: %w", err)
}
// find algorithm
if _, ok := token.Header["alg"]; !ok {
return nil, fmt.Errorf("kid not found")
}
alg := token.Header["alg"].(string)
if key.Alg() != alg {
return nil, fmt.Errorf("signing method doesn't match")
}
// get the public key
publicKey, err := key.PublicKey()
if err != nil {
return nil, fmt.Errorf("invalid public key: %w", err)
}
return publicKey, nil
}
func (v *auth0Validator) Cancel() {
v.certs.Cancel()
}
func (v *auth0Validator) Cancel() {}

View File

@ -0,0 +1,60 @@
package iam
import (
"net/http"
"github.com/datarhei/core/v16/iam"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
type Config struct {
// Skipper defines a function to skip middleware.
Skipper middleware.Skipper
IAM iam.IAM
}
var DefaultConfig = Config{
Skipper: middleware.DefaultSkipper,
IAM: nil,
}
func New() echo.MiddlewareFunc {
return NewWithConfig(DefaultConfig)
}
func NewWithConfig(config Config) echo.MiddlewareFunc {
if config.Skipper == nil {
config.Skipper = DefaultConfig.Skipper
}
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
if config.Skipper(c) {
return next(c)
}
if config.IAM == nil {
return next(c)
}
user := c.Get("user").(string)
if len(user) == 0 {
user = "$anon"
}
domain := c.QueryParam("group")
if len(domain) == 0 {
domain = "$none"
}
resource := c.Request().URL.Path
action := c.Request().Method
if !config.IAM.Enforce(user, domain, resource, action) {
return echo.NewHTTPError(http.StatusForbidden)
}
return next(c)
}
}
}

View File

@ -43,6 +43,7 @@ import (
"github.com/datarhei/core/v16/http/jwt"
"github.com/datarhei/core/v16/http/router"
"github.com/datarhei/core/v16/http/validator"
"github.com/datarhei/core/v16/iam"
"github.com/datarhei/core/v16/log"
"github.com/datarhei/core/v16/monitor"
"github.com/datarhei/core/v16/net"
@ -92,6 +93,7 @@ type Config struct {
Sessions session.RegistryReader
Router router.Router
ReadOnly bool
IAM iam.IAM
}
type CorsConfig struct {

21
iam/access.go Normal file
View File

@ -0,0 +1,21 @@
package iam
type AccessEnforcer interface {
Enforce(name, domain, resource, action string) bool
}
type AccessManager interface {
AccessEnforcer
AddPolicy()
}
type access struct {
}
func NewAccessManager() (AccessManager, error) {
return &access{}, nil
}
func (a *access) AddPolicy() {}
func (a *access) Enforce(name, domain, resource, action string) bool { return false }

26
iam/iam.go Normal file
View File

@ -0,0 +1,26 @@
package iam
type IAM interface {
Enforce(user, domain, resource, action string) bool
GetIdentity(name string) IdentityVerifier
GetIdentityByAuth0(name string) IdentityVerifier
}
type iam struct{}
func NewIAM() (IAM, error) {
return &iam{}, nil
}
func (i *iam) Enforce(user, domain, resource, action string) bool {
return false
}
func (i *iam) GetIdentity(name string) IdentityVerifier {
return nil
}
func (i *iam) GetIdentityByAuth0(name string) IdentityVerifier {
return nil
}

440
iam/identity.go Normal file
View File

@ -0,0 +1,440 @@
package iam
import (
"fmt"
"regexp"
"sync"
"github.com/datarhei/core/v16/http/jwt/jwks"
jwtgo "github.com/golang-jwt/jwt/v4"
)
// Auth0
// there needs to be a mapping from the Auth.User to Name
// the same Auth0.User can't have multiple identities
// the whole jwks will be part of this package
type User struct {
Name string `json:"name"`
Superuser bool `json:"superuser"`
Auth struct {
API struct {
Userpass struct {
Enable bool `json:"enable"`
Password string `json:"password"`
} `json:"userpass"`
Auth0 struct {
Enable bool `json:"enable"`
User string `json:"user"`
Tenant Auth0Tenant `json:"tenant"`
} `json:"auth0"`
} `json:"api"`
Services struct {
Basic struct {
Enable bool `json:"enable"`
Password string `json:"password"`
} `json:"basic"`
Token string `json:"token"`
} `json:"services"`
} `json:"auth"`
}
func (u *User) validate() error {
if len(u.Name) == 0 {
return fmt.Errorf("the name is required")
}
re := regexp.MustCompile(`[^A-Za-z0-9_-]`)
if re.MatchString(u.Name) {
return fmt.Errorf("the name can only the contain [A-Za-z0-9_-]")
}
if u.Auth.API.Userpass.Enable && len(u.Auth.API.Userpass.Password) == 0 {
return fmt.Errorf("a password for API login is required")
}
if u.Auth.API.Auth0.Enable && len(u.Auth.API.Auth0.User) == 0 {
return fmt.Errorf("a user for Auth0 login is required")
}
if u.Auth.Services.Basic.Enable && len(u.Auth.Services.Basic.Password) == 0 {
return fmt.Errorf("a password for service basic auth is required")
}
return nil
}
func (u *User) marshalIdentity() *identity {
i := &identity{
user: *u,
}
return i
}
type identity struct {
user User
tenant *auth0Tenant
valid bool
lock sync.RWMutex
}
func (i *identity) VerifyAPIPassword(password string) bool {
i.lock.RLock()
defer i.lock.RUnlock()
if !i.isValid() {
return false
}
if !i.user.Auth.API.Userpass.Enable {
return false
}
return i.user.Auth.API.Userpass.Password == password
}
func (i *identity) VerifyAPIAuth0(jwt string) bool {
i.lock.RLock()
defer i.lock.RUnlock()
if !i.isValid() {
return false
}
if !i.user.Auth.API.Auth0.Enable {
return false
}
p := &jwtgo.Parser{}
token, _, err := p.ParseUnverified(jwt, jwtgo.MapClaims{})
if err != nil {
return false
}
var subject string
if claims, ok := token.Claims.(jwtgo.MapClaims); ok {
if sub, ok := claims["sub"]; ok {
subject = sub.(string)
}
}
if subject != i.user.Auth.API.Auth0.User {
return false
}
var issuer string
if claims, ok := token.Claims.(jwtgo.MapClaims); ok {
if iss, ok := claims["iss"]; ok {
issuer = iss.(string)
}
}
if issuer != i.tenant.issuer {
return false
}
token, err = jwtgo.Parse(jwt, i.auth0KeyFunc)
if err != nil {
return false
}
if !token.Valid {
return false
}
return true
}
func (i *identity) auth0KeyFunc(token *jwtgo.Token) (interface{}, error) {
// Verify 'aud' claim
checkAud := token.Claims.(jwtgo.MapClaims).VerifyAudience(i.tenant.audience, false)
if !checkAud {
return nil, fmt.Errorf("invalid audience")
}
// Verify 'iss' claim
checkIss := token.Claims.(jwtgo.MapClaims).VerifyIssuer(i.tenant.issuer, false)
if !checkIss {
return nil, fmt.Errorf("invalid issuer")
}
// Verify 'sub' claim
if _, ok := token.Claims.(jwtgo.MapClaims)["sub"]; !ok {
return nil, fmt.Errorf("sub claim is required")
}
// find the key
if _, ok := token.Header["kid"]; !ok {
return nil, fmt.Errorf("kid not found")
}
kid := token.Header["kid"].(string)
key, err := i.tenant.certs.Key(kid)
if err != nil {
return nil, fmt.Errorf("no cert for kid found: %w", err)
}
// find algorithm
if _, ok := token.Header["alg"]; !ok {
return nil, fmt.Errorf("kid not found")
}
alg := token.Header["alg"].(string)
if key.Alg() != alg {
return nil, fmt.Errorf("signing method doesn't match")
}
// get the public key
publicKey, err := key.PublicKey()
if err != nil {
return nil, fmt.Errorf("invalid public key: %w", err)
}
return publicKey, nil
}
func (i *identity) VerifyServiceBasicAuth(password string) bool {
i.lock.RLock()
defer i.lock.RUnlock()
if !i.isValid() {
return false
}
if !i.user.Auth.Services.Basic.Enable {
return false
}
return i.user.Auth.Services.Basic.Password == password
}
func (i *identity) VerifyServiceToken(token string) bool {
i.lock.RLock()
defer i.lock.RUnlock()
if !i.isValid() {
return false
}
return i.user.Auth.Services.Token == token
}
func (i *identity) isValid() bool {
return i.valid
}
func (i *identity) IsSuperuser() bool {
i.lock.RLock()
defer i.lock.RUnlock()
return i.user.Superuser
}
type IdentityVerifier interface {
VerifyAPIPassword(password string) bool
VerifyAPIAuth0(jwt string) bool
VerifyServiceBasicAuth(password string) bool
VerifyServiceToken(token string) bool
IsSuperuser() bool
}
type IdentityManager interface {
Create(identity User) error
Remove(name string) error
Get(name string) (User, error)
GetVerifier(name string) (IdentityVerifier, error)
Rename(oldname, newname string) error
Update(name string, identity User) error
Load(path string) error
Save(path string) error
}
type Auth0Tenant struct {
Domain string
Audience string
ClientID string
}
func (t *Auth0Tenant) key() string {
return t.Domain + t.Audience
}
type auth0Tenant struct {
domain string
issuer string
audience string
clientIDs []string
certs jwks.JWKS
}
func newAuth0Tenant(tenant Auth0Tenant) (*auth0Tenant, error) {
t := &auth0Tenant{
domain: tenant.Domain,
issuer: "https://" + tenant.Domain + "/",
audience: tenant.Audience,
clientIDs: []string{tenant.ClientID},
certs: nil,
}
url := t.issuer + "/.well-known/jwks.json"
certs, err := jwks.NewFromURL(url, jwks.Config{})
if err != nil {
return nil, err
}
t.certs = certs
return t, nil
}
type identityManager struct {
identities map[string]*identity
tenants map[string]*auth0Tenant
auth0UserIdentityMap map[string]string
lock sync.RWMutex
}
func NewIdentityManager() (IdentityManager, error) {
return &identityManager{
identities: map[string]*identity{},
tenants: map[string]*auth0Tenant{},
auth0UserIdentityMap: map[string]string{},
}, nil
}
func (i *identityManager) Create(u User) error {
if err := u.validate(); err != nil {
return err
}
i.lock.Lock()
defer i.lock.Unlock()
_, ok := i.identities[u.Name]
if ok {
return fmt.Errorf("identity already exists")
}
identity := u.marshalIdentity()
if identity.user.Auth.API.Auth0.Enable {
if _, ok := i.auth0UserIdentityMap[identity.user.Auth.API.Auth0.User]; ok {
return fmt.Errorf("the Auth0 user has already an identity")
}
auth0Key := identity.user.Auth.API.Auth0.Tenant.key()
if _, ok := i.tenants[auth0Key]; !ok {
tenant, err := newAuth0Tenant(identity.user.Auth.API.Auth0.Tenant)
if err != nil {
return err
}
i.tenants[auth0Key] = tenant
}
}
i.identities[identity.user.Name] = identity
return nil
}
func (i *identityManager) Update(name string, identity User) error {
return nil
}
func (i *identityManager) Remove(name string) error {
i.lock.Lock()
defer i.lock.Unlock()
user, ok := i.identities[name]
if !ok {
return nil
}
delete(i.identities, name)
user.lock.Lock()
user.valid = false
user.lock.Unlock()
return nil
}
func (i *identityManager) getIdentity(name string) (*identity, error) {
i.lock.RLock()
defer i.lock.RUnlock()
identity, ok := i.identities[name]
if !ok {
return nil, fmt.Errorf("not found")
}
return identity, nil
}
func (i *identityManager) Get(name string) (User, error) {
i.lock.RLock()
defer i.lock.RUnlock()
identity, ok := i.identities[name]
if !ok {
return User{}, fmt.Errorf("not found")
}
return identity.user, nil
}
func (i *identityManager) GetVerifier(name string) (IdentityVerifier, error) {
i.lock.RLock()
defer i.lock.RUnlock()
identity, ok := i.identities[name]
if !ok {
return nil, fmt.Errorf("not found")
}
return identity, nil
}
func (i *identityManager) Rename(oldname, newname string) error {
i.lock.Lock()
defer i.lock.Unlock()
identity, ok := i.identities[oldname]
if !ok {
return nil
}
if _, ok := i.identities[newname]; ok {
return fmt.Errorf("the new name already exists")
}
delete(i.identities, oldname)
identity.user.Name = newname
i.identities[newname] = identity
return nil
}
func (i *identityManager) Load(path string) error {
return fmt.Errorf("not implemented")
}
func (i *identityManager) Save(path string) error {
return fmt.Errorf("not implemented")
}

View File

@ -12,6 +12,7 @@ import (
"sync"
"time"
"github.com/datarhei/core/v16/iam"
"github.com/datarhei/core/v16/log"
"github.com/datarhei/core/v16/session"
@ -195,6 +196,8 @@ type Config struct {
// ListenAndServe, so it's not possible to modify the configuration
// with methods like tls.Config.SetSessionTicketKeys.
TLSConfig *tls.Config
IAM iam.IAM
}
// Server represents a RTMP server

View File

@ -10,6 +10,7 @@ import (
"sync"
"time"
"github.com/datarhei/core/v16/iam"
"github.com/datarhei/core/v16/log"
"github.com/datarhei/core/v16/session"
srt "github.com/datarhei/gosrt"
@ -164,6 +165,8 @@ type Config struct {
Collector session.Collector
SRTLogTopics []string
IAM iam.IAM
}
// Server represents a SRT server