Add CreatedAt and UpdatedAt field to IAM user

This commit is contained in:
Ingo Oppermann
2023-09-18 15:18:21 +02:00
parent f31cb8eb0d
commit 92f2f6de8c
27 changed files with 3914 additions and 3147 deletions

View File

@@ -29,7 +29,7 @@ func (v versionInfo) MinorString() string {
// Version of the app // Version of the app
var Version = versionInfo{ var Version = versionInfo{
Major: 16, Major: 16,
Minor: 15, Minor: 16,
Patch: 0, Patch: 0,
} }

View File

@@ -2003,11 +2003,17 @@ const docTemplateClusterAPI = `{
"auth": { "auth": {
"$ref": "#/definitions/identity.UserAuth" "$ref": "#/definitions/identity.UserAuth"
}, },
"created_at": {
"type": "string"
},
"name": { "name": {
"type": "string" "type": "string"
}, },
"superuser": { "superuser": {
"type": "boolean" "type": "boolean"
},
"updated_at": {
"type": "string"
} }
} }
}, },

View File

@@ -1995,11 +1995,17 @@
"auth": { "auth": {
"$ref": "#/definitions/identity.UserAuth" "$ref": "#/definitions/identity.UserAuth"
}, },
"created_at": {
"type": "string"
},
"name": { "name": {
"type": "string" "type": "string"
}, },
"superuser": { "superuser": {
"type": "boolean" "type": "boolean"
},
"updated_at": {
"type": "string"
} }
} }
}, },

View File

@@ -587,10 +587,14 @@ definitions:
type: string type: string
auth: auth:
$ref: '#/definitions/identity.UserAuth' $ref: '#/definitions/identity.UserAuth'
created_at:
type: string
name: name:
type: string type: string
superuser: superuser:
type: boolean type: boolean
updated_at:
type: string
type: object type: object
identity.UserAuth: identity.UserAuth:
properties: properties:

127
cluster/store/identity.go Normal file
View File

@@ -0,0 +1,127 @@
package store
import (
"fmt"
"time"
)
func (s *store) addIdentity(cmd CommandAddIdentity) error {
s.lock.Lock()
defer s.lock.Unlock()
err := s.data.Users.userlist.Add(cmd.Identity)
if err != nil {
return fmt.Errorf("the identity with the name '%s' already exists", cmd.Identity.Name)
}
now := time.Now()
s.data.Users.UpdatedAt = now
cmd.Identity.CreatedAt = now
cmd.Identity.UpdatedAt = now
s.data.Users.Users[cmd.Identity.Name] = cmd.Identity
return nil
}
func (s *store) updateIdentity(cmd CommandUpdateIdentity) error {
s.lock.Lock()
defer s.lock.Unlock()
if cmd.Name == "$anon" {
return fmt.Errorf("the identity with the name '%s' can't be updated", cmd.Name)
}
oldUser, err := s.data.Users.userlist.Get(cmd.Name)
if err != nil {
return fmt.Errorf("the identity with the name '%s' doesn't exist", cmd.Name)
}
o, ok := s.data.Users.Users[oldUser.Name]
if !ok {
return fmt.Errorf("the identity with the name '%s' doesn't exist", cmd.Name)
}
err = s.data.Users.userlist.Update(cmd.Name, cmd.Identity)
if err != nil {
return err
}
user, err := s.data.Users.userlist.Get(cmd.Identity.Name)
if err != nil {
return fmt.Errorf("the identity with the name '%s' doesn't exist", cmd.Identity.Name)
}
now := time.Now()
user.CreatedAt = o.CreatedAt
user.UpdatedAt = now
s.data.Users.UpdatedAt = now
delete(s.data.Users.Users, oldUser.Name)
s.data.Users.Users[user.Name] = user
s.data.Policies.UpdatedAt = now
policies := s.data.Policies.Policies[oldUser.Name]
delete(s.data.Policies.Policies, oldUser.Name)
s.data.Policies.Policies[user.Name] = policies
return nil
}
func (s *store) removeIdentity(cmd CommandRemoveIdentity) error {
s.lock.Lock()
defer s.lock.Unlock()
user, err := s.data.Users.userlist.Get(cmd.Name)
if err != nil {
return nil
}
s.data.Users.userlist.Delete(user.Name)
delete(s.data.Users.Users, user.Name)
s.data.Users.UpdatedAt = time.Now()
delete(s.data.Policies.Policies, user.Name)
s.data.Policies.UpdatedAt = time.Now()
return nil
}
func (s *store) ListUsers() Users {
s.lock.RLock()
defer s.lock.RUnlock()
u := Users{
UpdatedAt: s.data.Users.UpdatedAt,
}
for _, user := range s.data.Users.Users {
u.Users = append(u.Users, user)
}
return u
}
func (s *store) GetUser(name string) Users {
s.lock.RLock()
defer s.lock.RUnlock()
u := Users{
UpdatedAt: s.data.Users.UpdatedAt,
}
user, err := s.data.Users.userlist.Get(name)
if err != nil {
return u
}
u.UpdatedAt = user.UpdatedAt
if user, ok := s.data.Users.Users[user.Name]; ok {
u.Users = append(u.Users, user)
}
return u
}

View File

@@ -0,0 +1,445 @@
package store
import (
"testing"
"time"
"github.com/datarhei/core/v16/iam/access"
"github.com/datarhei/core/v16/iam/identity"
"github.com/stretchr/testify/require"
)
func TestAddIdentityCommand(t *testing.T) {
s, err := createStore()
require.NoError(t, err)
identity := identity.User{
Name: "foobar",
}
err = s.applyCommand(Command{
Operation: OpAddIdentity,
Data: CommandAddIdentity{
Identity: identity,
},
})
require.NoError(t, err)
require.Equal(t, 1, len(s.data.Users.Users))
}
func TestAddIdentity(t *testing.T) {
s, err := createStore()
require.NoError(t, err)
idty := identity.User{
Name: "foobar",
}
err = s.addIdentity(CommandAddIdentity{
Identity: idty,
})
require.NoError(t, err)
require.Equal(t, 1, len(s.data.Users.Users))
require.Equal(t, 0, len(s.data.Policies.Policies))
err = s.addIdentity(CommandAddIdentity{
Identity: idty,
})
require.Error(t, err)
require.Equal(t, 1, len(s.data.Users.Users))
require.Equal(t, 0, len(s.data.Policies.Policies))
u := s.GetUser("foobar")
require.Equal(t, 1, len(u.Users))
user := u.Users[0]
require.Equal(t, user.CreatedAt, user.UpdatedAt)
require.NotEqual(t, time.Time{}, user.CreatedAt)
}
func TestAddIdentityWithAlias(t *testing.T) {
s, err := createStore()
require.NoError(t, err)
idty := identity.User{
Name: "foobar",
}
err = s.addIdentity(CommandAddIdentity{
Identity: idty,
})
require.NoError(t, err)
idty = identity.User{
Name: "foobaz",
Alias: "foobar",
}
err = s.addIdentity(CommandAddIdentity{
Identity: idty,
})
require.Error(t, err)
idty = identity.User{
Name: "foobaz",
Alias: "foobaz",
}
err = s.addIdentity(CommandAddIdentity{
Identity: idty,
})
require.NoError(t, err)
idty = identity.User{
Name: "barfoo",
Alias: "foobaz",
}
err = s.addIdentity(CommandAddIdentity{
Identity: idty,
})
require.Error(t, err)
}
func TestRemoveIdentityCommand(t *testing.T) {
s, err := createStore()
require.NoError(t, err)
identity := identity.User{
Name: "foobar",
}
err = s.applyCommand(Command{
Operation: OpAddIdentity,
Data: CommandAddIdentity{
Identity: identity,
},
})
require.NoError(t, err)
require.Equal(t, 1, len(s.data.Users.Users))
err = s.applyCommand(Command{
Operation: OpRemoveIdentity,
Data: CommandRemoveIdentity{
Name: "foobar",
},
})
require.NoError(t, err)
require.Equal(t, 0, len(s.data.Users.Users))
}
func TestRemoveIdentity(t *testing.T) {
s, err := createStore()
require.NoError(t, err)
identity := identity.User{
Name: "foobar",
}
err = s.addIdentity(CommandAddIdentity{
Identity: identity,
})
require.NoError(t, err)
require.Equal(t, 1, len(s.data.Users.Users))
require.Equal(t, 0, len(s.data.Policies.Policies))
err = s.removeIdentity(CommandRemoveIdentity{
Name: "foobar",
})
require.NoError(t, err)
require.Equal(t, 0, len(s.data.Users.Users))
require.Equal(t, 0, len(s.data.Policies.Policies))
err = s.removeIdentity(CommandRemoveIdentity{
Name: "foobar",
})
require.NoError(t, err)
require.Equal(t, 0, len(s.data.Users.Users))
require.Equal(t, 0, len(s.data.Policies.Policies))
}
func TestRemoveIdentityWithAlias(t *testing.T) {
s, err := createStore()
require.NoError(t, err)
idty := identity.User{
Name: "foobar",
Alias: "foobaz",
}
err = s.addIdentity(CommandAddIdentity{
Identity: idty,
})
require.NoError(t, err)
require.Equal(t, 1, len(s.data.Users.Users))
err = s.removeIdentity(CommandRemoveIdentity{
Name: "foobaz",
})
require.NoError(t, err)
require.Equal(t, 0, len(s.data.Users.Users))
err = s.removeIdentity(CommandRemoveIdentity{
Name: "foobar",
})
require.NoError(t, err)
require.Equal(t, 0, len(s.data.Users.Users))
}
func TestUpdateUserCommand(t *testing.T) {
s, err := createStore()
require.NoError(t, err)
idty1 := identity.User{
Name: "foobar1",
}
idty2 := identity.User{
Name: "foobar2",
}
err = s.applyCommand(Command{
Operation: OpAddIdentity,
Data: CommandAddIdentity{
Identity: idty1,
},
})
require.NoError(t, err)
require.Equal(t, 1, len(s.data.Users.Users))
err = s.applyCommand(Command{
Operation: OpAddIdentity,
Data: CommandAddIdentity{
Identity: idty2,
},
})
require.NoError(t, err)
require.Equal(t, 2, len(s.data.Users.Users))
err = s.applyCommand(Command{
Operation: OpUpdateIdentity,
Data: CommandUpdateIdentity{
Name: "foobar1",
Identity: identity.User{
Name: "foobar3",
},
},
})
require.NoError(t, err)
require.Equal(t, 2, len(s.data.Users.Users))
}
func TestUpdateIdentity(t *testing.T) {
s, err := createStore()
require.NoError(t, err)
idty1 := identity.User{
Name: "foobar1",
}
idty2 := identity.User{
Name: "foobar2",
}
err = s.addIdentity(CommandAddIdentity{
Identity: idty1,
})
require.NoError(t, err)
require.Equal(t, 1, len(s.data.Users.Users))
err = s.addIdentity(CommandAddIdentity{
Identity: idty2,
})
require.NoError(t, err)
require.Equal(t, 2, len(s.data.Users.Users))
foobar := s.GetUser("foobar1").Users[0]
require.True(t, foobar.CreatedAt.Equal(foobar.UpdatedAt))
require.False(t, time.Time{}.Equal(foobar.CreatedAt))
idty := identity.User{
Name: "foobaz",
}
err = s.updateIdentity(CommandUpdateIdentity{
Name: "foobar",
Identity: idty,
})
require.Error(t, err)
require.Equal(t, 2, len(s.data.Users.Users))
idty.Name = "foobar2"
err = s.updateIdentity(CommandUpdateIdentity{
Name: "foobar1",
Identity: idty,
})
require.Error(t, err)
require.Equal(t, 2, len(s.data.Users.Users))
idty.Name = "foobaz"
err = s.updateIdentity(CommandUpdateIdentity{
Name: "foobar1",
Identity: idty,
})
require.NoError(t, err)
require.Equal(t, 2, len(s.data.Users.Users))
u := s.GetUser("foobar1")
require.Empty(t, u.Users)
u = s.GetUser("foobar2")
require.NotEmpty(t, u.Users)
u = s.GetUser("foobaz")
require.NotEmpty(t, u.Users)
require.True(t, u.Users[0].CreatedAt.Equal(foobar.CreatedAt))
require.False(t, u.Users[0].UpdatedAt.Equal(foobar.UpdatedAt))
require.False(t, time.Time{}.Equal(foobar.CreatedAt))
}
func TestUpdateIdentityWithAlias(t *testing.T) {
s, err := createStore()
require.NoError(t, err)
idty1 := identity.User{
Name: "foobar1",
Alias: "fooalias1",
}
idty2 := identity.User{
Name: "foobar2",
Alias: "fooalias2",
}
err = s.addIdentity(CommandAddIdentity{
Identity: idty1,
})
require.NoError(t, err)
require.Equal(t, 1, len(s.data.Users.Users))
err = s.addIdentity(CommandAddIdentity{
Identity: idty2,
})
require.NoError(t, err)
require.Equal(t, 2, len(s.data.Users.Users))
idty := identity.User{
Name: "foobaz",
}
err = s.updateIdentity(CommandUpdateIdentity{
Name: "foobar",
Identity: idty,
})
require.Error(t, err)
require.Equal(t, 2, len(s.data.Users.Users))
idty.Name = "foobar2"
err = s.updateIdentity(CommandUpdateIdentity{
Name: "foobar1",
Identity: idty,
})
require.Error(t, err)
require.Equal(t, 2, len(s.data.Users.Users))
idty.Name = "foobaz"
idty.Alias = "fooalias2"
err = s.updateIdentity(CommandUpdateIdentity{
Name: "foobar1",
Identity: idty,
})
require.Error(t, err)
require.Equal(t, 2, len(s.data.Users.Users))
idty.Name = "foobaz"
idty.Alias = "fooalias"
err = s.updateIdentity(CommandUpdateIdentity{
Name: "fooalias1",
Identity: idty,
})
require.NoError(t, err)
require.Equal(t, 2, len(s.data.Users.Users))
u := s.GetUser("foobar1")
require.Empty(t, u.Users)
u = s.GetUser("fooalias1")
require.Empty(t, u.Users)
u = s.GetUser("foobar2")
require.NotEmpty(t, u.Users)
u = s.GetUser("fooalias2")
require.NotEmpty(t, u.Users)
u = s.GetUser("foobaz")
require.NotEmpty(t, u.Users)
u = s.GetUser("fooalias")
require.NotEmpty(t, u.Users)
}
func TestUpdateIdentityWithPolicies(t *testing.T) {
s, err := createStore()
require.NoError(t, err)
idty1 := identity.User{
Name: "foobar",
}
policies := []access.Policy{
{
Name: "bla",
Domain: "bla",
Resource: "bla",
Actions: []string{},
},
{
Name: "foo",
Domain: "foo",
Resource: "foo",
Actions: []string{},
},
}
err = s.addIdentity(CommandAddIdentity{
Identity: idty1,
})
require.NoError(t, err)
require.Equal(t, 1, len(s.data.Users.Users))
err = s.setPolicies(CommandSetPolicies{
Name: "foobar",
Policies: policies,
})
require.NoError(t, err)
require.Equal(t, 2, len(s.data.Policies.Policies["foobar"]))
err = s.updateIdentity(CommandUpdateIdentity{
Name: "foobar",
Identity: idty1,
})
require.NoError(t, err)
require.Equal(t, 1, len(s.data.Users.Users))
require.Equal(t, 2, len(s.data.Policies.Policies["foobar"]))
idty2 := identity.User{
Name: "foobaz",
}
err = s.updateIdentity(CommandUpdateIdentity{
Name: "foobar",
Identity: idty2,
})
require.NoError(t, err)
require.Equal(t, 1, len(s.data.Users.Users))
require.Equal(t, 0, len(s.data.Policies.Policies["foobar"]))
require.Equal(t, 2, len(s.data.Policies.Policies["foobaz"]))
}

63
cluster/store/kvs.go Normal file
View File

@@ -0,0 +1,63 @@
package store
import (
"io/fs"
"strings"
"time"
)
func (s *store) setKV(cmd CommandSetKV) error {
s.lock.Lock()
defer s.lock.Unlock()
value := s.data.KVS[cmd.Key]
value.Value = cmd.Value
value.UpdatedAt = time.Now()
s.data.KVS[cmd.Key] = value
return nil
}
func (s *store) unsetKV(cmd CommandUnsetKV) error {
s.lock.Lock()
defer s.lock.Unlock()
if _, ok := s.data.KVS[cmd.Key]; !ok {
return fs.ErrNotExist
}
delete(s.data.KVS, cmd.Key)
return nil
}
func (s *store) ListKVS(prefix string) map[string]Value {
s.lock.RLock()
defer s.lock.RUnlock()
m := map[string]Value{}
for key, value := range s.data.KVS {
if !strings.HasPrefix(key, prefix) {
continue
}
m[key] = value
}
return m
}
func (s *store) GetFromKVS(key string) (Value, error) {
s.lock.RLock()
defer s.lock.RUnlock()
value, ok := s.data.KVS[key]
if !ok {
return Value{}, fs.ErrNotExist
}
return value, nil
}

113
cluster/store/kvs_test.go Normal file
View File

@@ -0,0 +1,113 @@
package store
import (
"io/fs"
"testing"
"github.com/stretchr/testify/require"
)
func TestSetKVCommand(t *testing.T) {
s, err := createStore()
require.NoError(t, err)
err = s.applyCommand(Command{
Operation: OpSetKV,
Data: CommandSetKV{
Key: "foo",
Value: "bar",
},
})
require.NoError(t, err)
_, ok := s.data.KVS["foo"]
require.True(t, ok)
}
func TestSetKV(t *testing.T) {
s, err := createStore()
require.NoError(t, err)
err = s.setKV(CommandSetKV{
Key: "foo",
Value: "bar",
})
require.NoError(t, err)
value, err := s.GetFromKVS("foo")
require.NoError(t, err)
require.Equal(t, "bar", value.Value)
updatedAt := value.UpdatedAt
err = s.setKV(CommandSetKV{
Key: "foo",
Value: "baz",
})
require.NoError(t, err)
value, err = s.GetFromKVS("foo")
require.NoError(t, err)
require.Equal(t, "baz", value.Value)
require.Greater(t, value.UpdatedAt, updatedAt)
}
func TestUnsetKVCommand(t *testing.T) {
s, err := createStore()
require.NoError(t, err)
err = s.applyCommand(Command{
Operation: OpSetKV,
Data: CommandSetKV{
Key: "foo",
Value: "bar",
},
})
require.NoError(t, err)
_, ok := s.data.KVS["foo"]
require.True(t, ok)
err = s.applyCommand(Command{
Operation: OpUnsetKV,
Data: CommandUnsetKV{
Key: "foo",
},
})
require.NoError(t, err)
_, ok = s.data.KVS["foo"]
require.False(t, ok)
err = s.applyCommand(Command{
Operation: OpUnsetKV,
Data: CommandUnsetKV{
Key: "foo",
},
})
require.Error(t, err)
require.Equal(t, fs.ErrNotExist, err)
}
func TestUnsetKV(t *testing.T) {
s, err := createStore()
require.NoError(t, err)
err = s.setKV(CommandSetKV{
Key: "foo",
Value: "bar",
})
require.NoError(t, err)
_, err = s.GetFromKVS("foo")
require.NoError(t, err)
err = s.unsetKV(CommandUnsetKV{
Key: "foo",
})
require.NoError(t, err)
_, err = s.GetFromKVS("foo")
require.Error(t, err)
require.Equal(t, fs.ErrNotExist, err)
}

74
cluster/store/lock.go Normal file
View File

@@ -0,0 +1,74 @@
package store
import (
"fmt"
"time"
)
func (s *store) createLock(cmd CommandCreateLock) error {
s.lock.Lock()
defer s.lock.Unlock()
validUntil, ok := s.data.Locks[cmd.Name]
if ok {
if time.Now().Before(validUntil) {
return fmt.Errorf("the lock with the ID '%s' already exists", cmd.Name)
}
}
s.data.Locks[cmd.Name] = cmd.ValidUntil
return nil
}
func (s *store) deleteLock(cmd CommandDeleteLock) error {
s.lock.Lock()
defer s.lock.Unlock()
if _, ok := s.data.Locks[cmd.Name]; !ok {
return nil
}
delete(s.data.Locks, cmd.Name)
return nil
}
func (s *store) clearLocks(cmd CommandClearLocks) error {
s.lock.Lock()
defer s.lock.Unlock()
for name, validUntil := range s.data.Locks {
if time.Now().Before(validUntil) {
// Lock is still valid
continue
}
delete(s.data.Locks, name)
}
return nil
}
func (s *store) HasLock(name string) bool {
s.lock.RLock()
defer s.lock.RUnlock()
_, ok := s.data.Locks[name]
return ok
}
func (s *store) ListLocks() map[string]time.Time {
s.lock.RLock()
defer s.lock.RUnlock()
m := map[string]time.Time{}
for key, value := range s.data.Locks {
m[key] = value
}
return m
}

167
cluster/store/lock_test.go Normal file
View File

@@ -0,0 +1,167 @@
package store
import (
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestCreateLockCommand(t *testing.T) {
s, err := createStore()
require.NoError(t, err)
err = s.applyCommand(Command{
Operation: OpCreateLock,
Data: CommandCreateLock{
Name: "foobar",
ValidUntil: time.Now().Add(3 * time.Second),
},
})
require.NoError(t, err)
_, ok := s.data.Locks["foobar"]
require.True(t, ok)
}
func TestCreateLock(t *testing.T) {
s, err := createStore()
require.NoError(t, err)
cmd := CommandCreateLock{
Name: "foobar",
ValidUntil: time.Now().Add(3 * time.Second),
}
err = s.createLock(cmd)
require.NoError(t, err)
err = s.createLock(cmd)
require.Error(t, err)
require.Eventually(t, func() bool {
err = s.createLock(cmd)
return err == nil
}, 5*time.Second, time.Second)
}
func TestDeleteLockCommand(t *testing.T) {
s, err := createStore()
require.NoError(t, err)
err = s.applyCommand(Command{
Operation: OpCreateLock,
Data: CommandCreateLock{
Name: "foobar",
ValidUntil: time.Now().Add(10 * time.Second),
},
})
require.NoError(t, err)
_, ok := s.data.Locks["foobar"]
require.True(t, ok)
err = s.applyCommand(Command{
Operation: OpDeleteLock,
Data: CommandDeleteLock{
Name: "foobar",
},
})
require.NoError(t, err)
_, ok = s.data.Locks["foobar"]
require.False(t, ok)
}
func TestDeleteLock(t *testing.T) {
s, err := createStore()
require.NoError(t, err)
err = s.deleteLock(CommandDeleteLock{
Name: "foobar",
})
require.NoError(t, err)
cmd := CommandCreateLock{
Name: "foobar",
ValidUntil: time.Now().Add(10 * time.Second),
}
err = s.createLock(cmd)
require.NoError(t, err)
err = s.createLock(cmd)
require.Error(t, err)
err = s.deleteLock(CommandDeleteLock{
Name: "foobar",
})
require.NoError(t, err)
err = s.createLock(cmd)
require.NoError(t, err)
}
func TestClearLocksCommand(t *testing.T) {
s, err := createStore()
require.NoError(t, err)
err = s.applyCommand(Command{
Operation: OpCreateLock,
Data: CommandCreateLock{
Name: "foobar",
ValidUntil: time.Now().Add(3 * time.Second),
},
})
require.NoError(t, err)
_, ok := s.data.Locks["foobar"]
require.True(t, ok)
err = s.applyCommand(Command{
Operation: OpClearLocks,
Data: CommandClearLocks{},
})
require.NoError(t, err)
_, ok = s.data.Locks["foobar"]
require.True(t, ok)
time.Sleep(3 * time.Second)
err = s.applyCommand(Command{
Operation: OpClearLocks,
Data: CommandClearLocks{},
})
require.NoError(t, err)
_, ok = s.data.Locks["foobar"]
require.False(t, ok)
}
func TestClearLocks(t *testing.T) {
s, err := createStore()
require.NoError(t, err)
cmd := CommandCreateLock{
Name: "foobar",
ValidUntil: time.Now().Add(3 * time.Second),
}
err = s.createLock(cmd)
require.NoError(t, err)
err = s.clearLocks(CommandClearLocks{})
require.NoError(t, err)
err = s.createLock(cmd)
require.Error(t, err)
time.Sleep(3 * time.Second)
err = s.clearLocks(CommandClearLocks{})
require.NoError(t, err)
err = s.createLock(cmd)
require.NoError(t, err)
}

77
cluster/store/policy.go Normal file
View File

@@ -0,0 +1,77 @@
package store
import (
"fmt"
"time"
)
func (s *store) setPolicies(cmd CommandSetPolicies) error {
s.lock.Lock()
defer s.lock.Unlock()
now := time.Now()
if cmd.Name != "$anon" {
user, err := s.data.Users.userlist.Get(cmd.Name)
if err != nil {
return fmt.Errorf("the identity with the name '%s' doesn't exist", cmd.Name)
}
u, ok := s.data.Users.Users[user.Name]
if !ok {
return fmt.Errorf("the identity with the name '%s' doesn't exist", cmd.Name)
}
u.UpdatedAt = now
s.data.Users.Users[user.Name] = u
}
for i, p := range cmd.Policies {
if len(p.Domain) != 0 {
continue
}
p.Domain = "$none"
cmd.Policies[i] = p
}
delete(s.data.Policies.Policies, cmd.Name)
s.data.Policies.Policies[cmd.Name] = cmd.Policies
s.data.Policies.UpdatedAt = now
return nil
}
func (s *store) ListPolicies() Policies {
s.lock.RLock()
defer s.lock.RUnlock()
p := Policies{
UpdatedAt: s.data.Policies.UpdatedAt,
}
for _, policies := range s.data.Policies.Policies {
p.Policies = append(p.Policies, policies...)
}
return p
}
func (s *store) ListUserPolicies(name string) Policies {
s.lock.RLock()
defer s.lock.RUnlock()
p := Policies{
UpdatedAt: s.data.Policies.UpdatedAt,
}
user, err := s.data.Users.userlist.Get(name)
if err != nil {
return p
}
p.UpdatedAt = user.UpdatedAt
p.Policies = append(p.Policies, s.data.Policies.Policies[user.Name]...)
return p
}

View File

@@ -0,0 +1,108 @@
package store
import (
"testing"
"github.com/datarhei/core/v16/iam/access"
"github.com/datarhei/core/v16/iam/identity"
"github.com/stretchr/testify/require"
)
func TestSetPoliciesCommand(t *testing.T) {
s, err := createStore()
require.NoError(t, err)
identity := identity.User{
Name: "foobar",
}
err = s.applyCommand(Command{
Operation: OpAddIdentity,
Data: CommandAddIdentity{
Identity: identity,
},
})
require.NoError(t, err)
require.Equal(t, 1, len(s.data.Users.Users))
require.Equal(t, 0, len(s.data.Policies.Policies))
err = s.applyCommand(Command{
Operation: OpSetPolicies,
Data: CommandSetPolicies{
Name: "foobar",
Policies: []access.Policy{
{
Name: "bla",
Domain: "bla",
Resource: "bla",
Actions: []string{},
},
{
Name: "foo",
Domain: "foo",
Resource: "foo",
Actions: []string{},
},
},
},
})
require.NoError(t, err)
require.Equal(t, 1, len(s.data.Users.Users))
require.Equal(t, 2, len(s.data.Policies.Policies["foobar"]))
}
func TestSetPolicies(t *testing.T) {
s, err := createStore()
require.NoError(t, err)
identity := identity.User{
Name: "foobar",
}
policies := []access.Policy{
{
Name: "bla",
Domain: "bla",
Resource: "bla",
Actions: []string{},
},
{
Name: "foo",
Domain: "foo",
Resource: "foo",
Actions: []string{},
},
}
err = s.setPolicies(CommandSetPolicies{
Name: "foobar",
Policies: policies,
})
require.Error(t, err)
require.Equal(t, 0, len(s.data.Policies.Policies["foobar"]))
err = s.addIdentity(CommandAddIdentity{
Identity: identity,
})
require.NoError(t, err)
require.Equal(t, 1, len(s.data.Users.Users))
require.Equal(t, 0, len(s.data.Policies.Policies))
users := s.GetUser("foobar")
require.NotEmpty(t, users.Users)
updatedAt := users.Users[0].UpdatedAt
err = s.setPolicies(CommandSetPolicies{
Name: "foobar",
Policies: policies,
})
require.NoError(t, err)
require.Equal(t, 1, len(s.data.Users.Users))
require.Equal(t, 2, len(s.data.Policies.Policies["foobar"]))
users = s.GetUser("foobar")
require.NotEmpty(t, users.Users)
require.False(t, updatedAt.Equal(users.Users[0].UpdatedAt))
}

228
cluster/store/process.go Normal file
View File

@@ -0,0 +1,228 @@
package store
import (
"fmt"
"time"
"github.com/datarhei/core/v16/restream/app"
)
func (s *store) addProcess(cmd CommandAddProcess) error {
s.lock.Lock()
defer s.lock.Unlock()
id := cmd.Config.ProcessID().String()
if cmd.Config.LimitCPU <= 0 || cmd.Config.LimitMemory <= 0 {
return fmt.Errorf("the process with the ID '%s' must have limits defined", id)
}
_, ok := s.data.Process[id]
if ok {
return fmt.Errorf("the process with the ID '%s' already exists", id)
}
order := "stop"
if cmd.Config.Autostart {
order = "start"
cmd.Config.Autostart = false
}
now := time.Now()
s.data.Process[id] = Process{
CreatedAt: now,
UpdatedAt: now,
Config: cmd.Config,
Order: order,
Metadata: map[string]interface{}{},
}
return nil
}
func (s *store) removeProcess(cmd CommandRemoveProcess) error {
s.lock.Lock()
defer s.lock.Unlock()
id := cmd.ID.String()
_, ok := s.data.Process[id]
if !ok {
return fmt.Errorf("the process with the ID '%s' doesn't exist", id)
}
delete(s.data.Process, id)
return nil
}
func (s *store) updateProcess(cmd CommandUpdateProcess) error {
s.lock.Lock()
defer s.lock.Unlock()
srcid := cmd.ID.String()
dstid := cmd.Config.ProcessID().String()
if cmd.Config.LimitCPU <= 0 || cmd.Config.LimitMemory <= 0 {
return fmt.Errorf("the process with the ID '%s' must have limits defined", dstid)
}
p, ok := s.data.Process[srcid]
if !ok {
return fmt.Errorf("the process with the ID '%s' doesn't exists", srcid)
}
if p.Config.Equal(cmd.Config) {
return nil
}
if srcid == dstid {
p.UpdatedAt = time.Now()
p.Config = cmd.Config
s.data.Process[srcid] = p
return nil
}
_, ok = s.data.Process[dstid]
if ok {
return fmt.Errorf("the process with the ID '%s' already exists", dstid)
}
now := time.Now()
p.CreatedAt = now
p.UpdatedAt = now
p.Config = cmd.Config
delete(s.data.Process, srcid)
s.data.Process[dstid] = p
return nil
}
func (s *store) setProcessOrder(cmd CommandSetProcessOrder) error {
s.lock.Lock()
defer s.lock.Unlock()
id := cmd.ID.String()
p, ok := s.data.Process[id]
if !ok {
return fmt.Errorf("the process with the ID '%s' doesn't exists", cmd.ID)
}
p.Order = cmd.Order
p.UpdatedAt = time.Now()
s.data.Process[id] = p
return nil
}
func (s *store) setProcessMetadata(cmd CommandSetProcessMetadata) error {
s.lock.Lock()
defer s.lock.Unlock()
id := cmd.ID.String()
p, ok := s.data.Process[id]
if !ok {
return fmt.Errorf("the process with the ID '%s' doesn't exists", cmd.ID)
}
if p.Metadata == nil {
p.Metadata = map[string]interface{}{}
}
if cmd.Data == nil {
delete(p.Metadata, cmd.Key)
} else {
p.Metadata[cmd.Key] = cmd.Data
}
p.UpdatedAt = time.Now()
s.data.Process[id] = p
return nil
}
func (s *store) setProcessError(cmd CommandSetProcessError) error {
s.lock.Lock()
defer s.lock.Unlock()
id := cmd.ID.String()
p, ok := s.data.Process[id]
if !ok {
return fmt.Errorf("the process with the ID '%s' doesn't exists", cmd.ID)
}
p.Error = cmd.Error
s.data.Process[id] = p
return nil
}
func (s *store) setProcessNodeMap(cmd CommandSetProcessNodeMap) error {
s.lock.Lock()
defer s.lock.Unlock()
s.data.ProcessNodeMap = cmd.Map
return nil
}
func (s *store) ListProcesses() []Process {
s.lock.RLock()
defer s.lock.RUnlock()
processes := []Process{}
for _, p := range s.data.Process {
processes = append(processes, Process{
CreatedAt: p.CreatedAt,
UpdatedAt: p.UpdatedAt,
Config: p.Config.Clone(),
Order: p.Order,
Metadata: p.Metadata,
Error: p.Error,
})
}
return processes
}
func (s *store) GetProcess(id app.ProcessID) (Process, error) {
s.lock.RLock()
defer s.lock.RUnlock()
process, ok := s.data.Process[id.String()]
if !ok {
return Process{}, fmt.Errorf("not found")
}
return Process{
CreatedAt: process.CreatedAt,
UpdatedAt: process.UpdatedAt,
Config: process.Config.Clone(),
Order: process.Order,
Metadata: process.Metadata,
Error: process.Error,
}, nil
}
func (s *store) GetProcessNodeMap() map[string]string {
s.lock.RLock()
defer s.lock.RUnlock()
m := map[string]string{}
for key, value := range s.data.ProcessNodeMap {
m[key] = value
}
return m
}

View File

@@ -0,0 +1,647 @@
package store
import (
"testing"
"github.com/datarhei/core/v16/restream/app"
"github.com/stretchr/testify/require"
)
func TestAddProcessCommand(t *testing.T) {
s, err := createStore()
require.NoError(t, err)
config := &app.Config{
ID: "foobar",
LimitCPU: 1,
LimitMemory: 1,
}
err = s.applyCommand(Command{
Operation: OpAddProcess,
Data: CommandAddProcess{
Config: config,
},
})
require.NoError(t, err)
require.NotEmpty(t, s.data.Process)
p, ok := s.data.Process["foobar@"]
require.True(t, ok)
require.NotZero(t, p.CreatedAt)
require.NotZero(t, p.UpdatedAt)
require.NotNil(t, p.Config)
require.True(t, p.Config.Equal(config))
require.NotNil(t, p.Metadata)
}
func TestAddProcess(t *testing.T) {
s, err := createStore()
require.NoError(t, err)
config := &app.Config{
ID: "foobar",
}
err = s.addProcess(CommandAddProcess{
Config: config,
})
require.Error(t, err)
require.Empty(t, s.data.Process)
config = &app.Config{
ID: "foobar",
LimitCPU: 1,
LimitMemory: 1,
}
err = s.addProcess(CommandAddProcess{
Config: config,
})
require.NoError(t, err)
require.Equal(t, 1, len(s.data.Process))
config = &app.Config{
ID: "foobar",
LimitCPU: 1,
LimitMemory: 1,
}
err = s.addProcess(CommandAddProcess{
Config: config,
})
require.Error(t, err)
require.Equal(t, 1, len(s.data.Process))
config = &app.Config{
ID: "foobar",
Domain: "barfoo",
LimitCPU: 1,
LimitMemory: 1,
}
err = s.addProcess(CommandAddProcess{
Config: config,
})
require.NoError(t, err)
require.Equal(t, 2, len(s.data.Process))
}
func TestRemoveProcessCommand(t *testing.T) {
s, err := createStore()
require.NoError(t, err)
config := &app.Config{
ID: "foobar",
LimitCPU: 1,
LimitMemory: 1,
}
err = s.applyCommand(Command{
Operation: OpAddProcess,
Data: CommandAddProcess{
Config: config,
},
})
require.NoError(t, err)
require.NotEmpty(t, s.data.Process)
err = s.applyCommand(Command{
Operation: OpRemoveProcess,
Data: CommandRemoveProcess{
ID: config.ProcessID(),
},
})
require.NoError(t, err)
require.Empty(t, s.data.Process)
err = s.applyCommand(Command{
Operation: OpRemoveProcess,
Data: CommandRemoveProcess{
ID: config.ProcessID(),
},
})
require.Error(t, err)
}
func TestRemoveProcess(t *testing.T) {
s, err := createStore()
require.NoError(t, err)
config1 := &app.Config{
ID: "foobar",
LimitCPU: 1,
LimitMemory: 1,
}
config2 := &app.Config{
ID: "foobar",
Domain: "barfoo",
LimitCPU: 1,
LimitMemory: 1,
}
err = s.removeProcess(CommandRemoveProcess{
ID: config1.ProcessID(),
})
require.Error(t, err)
err = s.removeProcess(CommandRemoveProcess{
ID: config2.ProcessID(),
})
require.Error(t, err)
err = s.addProcess(CommandAddProcess{
Config: config1,
})
require.NoError(t, err)
require.Equal(t, 1, len(s.data.Process))
err = s.addProcess(CommandAddProcess{
Config: config2,
})
require.NoError(t, err)
require.Equal(t, 2, len(s.data.Process))
err = s.removeProcess(CommandRemoveProcess{
ID: config1.ProcessID(),
})
require.NoError(t, err)
require.Equal(t, 1, len(s.data.Process))
err = s.removeProcess(CommandRemoveProcess{
ID: config2.ProcessID(),
})
require.NoError(t, err)
require.Empty(t, s.data.Process)
}
func TestUpdateProcessCommand(t *testing.T) {
s, err := createStore()
require.NoError(t, err)
config := &app.Config{
ID: "foobar",
LimitCPU: 1,
LimitMemory: 1,
}
pid := config.ProcessID()
err = s.applyCommand(Command{
Operation: OpAddProcess,
Data: CommandAddProcess{
Config: config,
},
})
require.NoError(t, err)
require.NotEmpty(t, s.data.Process)
config = &app.Config{
ID: "foobaz",
LimitCPU: 1,
LimitMemory: 1,
}
err = s.applyCommand(Command{
Operation: OpUpdateProcess,
Data: CommandUpdateProcess{
ID: pid,
Config: config,
},
})
require.NoError(t, err)
require.NotEmpty(t, s.data.Process)
}
func TestUpdateProcess(t *testing.T) {
s, err := createStore()
require.NoError(t, err)
config1 := &app.Config{
ID: "foobar",
LimitCPU: 1,
LimitMemory: 1,
}
config2 := &app.Config{
ID: "fooboz",
LimitCPU: 1,
LimitMemory: 1,
}
err = s.addProcess(CommandAddProcess{
Config: config1,
})
require.NoError(t, err)
require.Equal(t, 1, len(s.data.Process))
err = s.addProcess(CommandAddProcess{
Config: config2,
})
require.NoError(t, err)
require.Equal(t, 2, len(s.data.Process))
config := &app.Config{
ID: "foobaz",
LimitCPU: 1,
LimitMemory: 1,
}
err = s.updateProcess(CommandUpdateProcess{
ID: config.ProcessID(),
Config: config,
})
require.Error(t, err)
config.ID = "fooboz"
err = s.updateProcess(CommandUpdateProcess{
ID: config1.ProcessID(),
Config: config,
})
require.Error(t, err)
config.ID = "foobaz"
config.LimitCPU = 0
err = s.updateProcess(CommandUpdateProcess{
ID: config1.ProcessID(),
Config: config,
})
require.Error(t, err)
config.LimitCPU = 1
err = s.updateProcess(CommandUpdateProcess{
ID: config1.ProcessID(),
Config: config,
})
require.NoError(t, err)
require.Equal(t, 2, len(s.data.Process))
err = s.updateProcess(CommandUpdateProcess{
ID: config.ProcessID(),
Config: config,
})
require.NoError(t, err)
require.Equal(t, 2, len(s.data.Process))
config3 := &app.Config{
ID: config.ID,
LimitCPU: 1,
LimitMemory: 2,
}
err = s.updateProcess(CommandUpdateProcess{
ID: config.ProcessID(),
Config: config3,
})
require.NoError(t, err)
require.Equal(t, 2, len(s.data.Process))
_, err = s.GetProcess(config1.ProcessID())
require.Error(t, err)
_, err = s.GetProcess(config2.ProcessID())
require.NoError(t, err)
_, err = s.GetProcess(config.ProcessID())
require.NoError(t, err)
}
func TestSetProcessOrderCommand(t *testing.T) {
s, err := createStore()
require.NoError(t, err)
config := &app.Config{
ID: "foobar",
LimitCPU: 1,
LimitMemory: 1,
}
err = s.applyCommand(Command{
Operation: OpAddProcess,
Data: CommandAddProcess{
Config: config,
},
})
require.NoError(t, err)
require.NotEmpty(t, s.data.Process)
p, err := s.GetProcess(config.ProcessID())
require.NoError(t, err)
require.Equal(t, "stop", p.Order)
err = s.applyCommand(Command{
Operation: OpSetProcessOrder,
Data: CommandSetProcessOrder{
ID: config.ProcessID(),
Order: "start",
},
})
require.NoError(t, err)
p, err = s.GetProcess(config.ProcessID())
require.NoError(t, err)
require.Equal(t, "start", p.Order)
}
func TestSetProcessOrder(t *testing.T) {
s, err := createStore()
require.NoError(t, err)
config := &app.Config{
ID: "foobar",
LimitCPU: 1,
LimitMemory: 1,
}
err = s.setProcessOrder(CommandSetProcessOrder{
ID: config.ProcessID(),
Order: "start",
})
require.Error(t, err)
err = s.addProcess(CommandAddProcess{
Config: config,
})
require.NoError(t, err)
require.Equal(t, 1, len(s.data.Process))
err = s.setProcessOrder(CommandSetProcessOrder{
ID: config.ProcessID(),
Order: "start",
})
require.NoError(t, err)
err = s.setProcessOrder(CommandSetProcessOrder{
ID: config.ProcessID(),
Order: "stop",
})
require.NoError(t, err)
p, err := s.GetProcess(config.ProcessID())
require.NoError(t, err)
require.Equal(t, "stop", p.Order)
err = s.setProcessOrder(CommandSetProcessOrder{
ID: config.ProcessID(),
Order: "start",
})
require.NoError(t, err)
p, err = s.GetProcess(config.ProcessID())
require.NoError(t, err)
require.Equal(t, "start", p.Order)
}
func TestSetProcessMetadataCommand(t *testing.T) {
s, err := createStore()
require.NoError(t, err)
config := &app.Config{
ID: "foobar",
LimitCPU: 1,
LimitMemory: 1,
}
err = s.applyCommand(Command{
Operation: OpAddProcess,
Data: CommandAddProcess{
Config: config,
},
})
require.NoError(t, err)
require.NotEmpty(t, s.data.Process)
p, err := s.GetProcess(config.ProcessID())
require.NoError(t, err)
require.Empty(t, p.Metadata)
metadata := "bar"
err = s.applyCommand(Command{
Operation: OpSetProcessMetadata,
Data: CommandSetProcessMetadata{
ID: config.ProcessID(),
Key: "foo",
Data: metadata,
},
})
require.NoError(t, err)
p, err = s.GetProcess(config.ProcessID())
require.NoError(t, err)
require.NotEmpty(t, p.Metadata)
require.Equal(t, 1, len(p.Metadata))
require.Equal(t, "bar", p.Metadata["foo"])
}
func TestSetProcessMetadata(t *testing.T) {
s, err := createStore()
require.NoError(t, err)
config := &app.Config{
ID: "foobar",
LimitCPU: 1,
LimitMemory: 1,
}
err = s.setProcessMetadata(CommandSetProcessMetadata{
ID: config.ProcessID(),
Key: "foo",
Data: "bar",
})
require.Error(t, err)
err = s.addProcess(CommandAddProcess{
Config: config,
})
require.NoError(t, err)
require.Equal(t, 1, len(s.data.Process))
err = s.setProcessMetadata(CommandSetProcessMetadata{
ID: config.ProcessID(),
Key: "foo",
Data: "bar",
})
require.NoError(t, err)
err = s.setProcessMetadata(CommandSetProcessMetadata{
ID: config.ProcessID(),
Key: "faa",
Data: "boz",
})
require.NoError(t, err)
p, err := s.GetProcess(config.ProcessID())
require.NoError(t, err)
require.NotEmpty(t, p.Metadata)
require.Equal(t, 2, len(p.Metadata))
require.Equal(t, "bar", p.Metadata["foo"])
require.Equal(t, "boz", p.Metadata["faa"])
err = s.setProcessMetadata(CommandSetProcessMetadata{
ID: config.ProcessID(),
Key: "faa",
Data: nil,
})
require.NoError(t, err)
p, err = s.GetProcess(config.ProcessID())
require.NoError(t, err)
require.NotEmpty(t, p.Metadata)
require.Equal(t, 1, len(p.Metadata))
require.Equal(t, "bar", p.Metadata["foo"])
err = s.setProcessMetadata(CommandSetProcessMetadata{
ID: config.ProcessID(),
Key: "foo",
Data: "bor",
})
require.NoError(t, err)
p, err = s.GetProcess(config.ProcessID())
require.NoError(t, err)
require.NotEmpty(t, p.Metadata)
require.Equal(t, 1, len(p.Metadata))
require.Equal(t, "bor", p.Metadata["foo"])
}
func TestSetProcessErrorCommand(t *testing.T) {
s, err := createStore()
require.NoError(t, err)
config := &app.Config{
ID: "foobar",
LimitCPU: 1,
LimitMemory: 1,
}
err = s.applyCommand(Command{
Operation: OpAddProcess,
Data: CommandAddProcess{
Config: config,
},
})
require.NoError(t, err)
require.NotEmpty(t, s.data.Process)
p, err := s.GetProcess(config.ProcessID())
require.NoError(t, err)
require.Equal(t, "", p.Error)
err = s.applyCommand(Command{
Operation: OpSetProcessError,
Data: CommandSetProcessError{
ID: config.ProcessID(),
Error: "foobar",
},
})
require.NoError(t, err)
p, err = s.GetProcess(config.ProcessID())
require.NoError(t, err)
require.Equal(t, "foobar", p.Error)
}
func TestSetProcessError(t *testing.T) {
s, err := createStore()
require.NoError(t, err)
config := &app.Config{
ID: "foobar",
LimitCPU: 1,
LimitMemory: 1,
}
err = s.setProcessError(CommandSetProcessError{
ID: config.ProcessID(),
Error: "foobar",
})
require.Error(t, err)
err = s.addProcess(CommandAddProcess{
Config: config,
})
require.NoError(t, err)
require.Equal(t, 1, len(s.data.Process))
err = s.setProcessError(CommandSetProcessError{
ID: config.ProcessID(),
Error: "foobar",
})
require.NoError(t, err)
err = s.setProcessError(CommandSetProcessError{
ID: config.ProcessID(),
Error: "",
})
require.NoError(t, err)
p, err := s.GetProcess(config.ProcessID())
require.NoError(t, err)
require.Equal(t, "", p.Error)
err = s.setProcessError(CommandSetProcessError{
ID: config.ProcessID(),
Error: "foobar",
})
require.NoError(t, err)
p, err = s.GetProcess(config.ProcessID())
require.NoError(t, err)
require.Equal(t, "foobar", p.Error)
}
func TestSetProcessNodeMapCommand(t *testing.T) {
s, err := createStore()
require.NoError(t, err)
m1 := map[string]string{
"key": "value1",
}
err = s.applyCommand(Command{
Operation: OpSetProcessNodeMap,
Data: CommandSetProcessNodeMap{
Map: m1,
},
})
require.NoError(t, err)
require.Equal(t, m1, s.data.ProcessNodeMap)
}
func TestSetProcessNodeMap(t *testing.T) {
s, err := createStore()
require.NoError(t, err)
m1 := map[string]string{
"key": "value1",
}
err = s.setProcessNodeMap(CommandSetProcessNodeMap{
Map: m1,
})
require.NoError(t, err)
require.Equal(t, m1, s.data.ProcessNodeMap)
m2 := map[string]string{
"key": "value2",
}
err = s.setProcessNodeMap(CommandSetProcessNodeMap{
Map: m2,
})
require.NoError(t, err)
require.Equal(t, m2, s.data.ProcessNodeMap)
m := s.GetProcessNodeMap()
require.Equal(t, m2, m)
}

View File

@@ -4,8 +4,6 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"io/fs"
"strings"
"sync" "sync"
"time" "time"
@@ -167,6 +165,7 @@ type storeData struct {
Users struct { Users struct {
UpdatedAt time.Time UpdatedAt time.Time
Users map[string]identity.User Users map[string]identity.User
userlist identity.UserList
} }
Policies struct { Policies struct {
@@ -187,6 +186,7 @@ func (s *storeData) init() {
s.ProcessNodeMap = map[string]string{} s.ProcessNodeMap = map[string]string{}
s.Users.UpdatedAt = now s.Users.UpdatedAt = now
s.Users.Users = map[string]identity.User{} s.Users.Users = map[string]identity.User{}
s.Users.userlist = identity.NewUserList()
s.Policies.UpdatedAt = now s.Policies.UpdatedAt = now
s.Policies.Policies = map[string][]access.Policy{} s.Policies.Policies = map[string][]access.Policy{}
s.Locks = map[string]time.Time{} s.Locks = map[string]time.Time{}
@@ -244,7 +244,6 @@ func (s *store) Apply(entry *raft.Log) interface{} {
logger.Debug().WithField("operation", c.Operation).Log("") logger.Debug().WithField("operation", c.Operation).Log("")
err = s.applyCommand(c) err = s.applyCommand(c)
if err != nil { if err != nil {
logger.Debug().WithError(err).WithField("operation", c.Operation).Log("") logger.Debug().WithError(err).WithField("operation", c.Operation).Log("")
return err return err
@@ -410,342 +409,6 @@ func (s *store) applyCommand(c Command) error {
return err return err
} }
func (s *store) addProcess(cmd CommandAddProcess) error {
s.lock.Lock()
defer s.lock.Unlock()
id := cmd.Config.ProcessID().String()
if cmd.Config.LimitCPU <= 0 || cmd.Config.LimitMemory <= 0 {
return fmt.Errorf("the process with the ID '%s' must have limits defined", id)
}
_, ok := s.data.Process[id]
if ok {
return fmt.Errorf("the process with the ID '%s' already exists", id)
}
order := "stop"
if cmd.Config.Autostart {
order = "start"
cmd.Config.Autostart = false
}
now := time.Now()
s.data.Process[id] = Process{
CreatedAt: now,
UpdatedAt: now,
Config: cmd.Config,
Order: order,
Metadata: map[string]interface{}{},
}
return nil
}
func (s *store) removeProcess(cmd CommandRemoveProcess) error {
s.lock.Lock()
defer s.lock.Unlock()
id := cmd.ID.String()
_, ok := s.data.Process[id]
if !ok {
return fmt.Errorf("the process with the ID '%s' doesn't exist", id)
}
delete(s.data.Process, id)
return nil
}
func (s *store) updateProcess(cmd CommandUpdateProcess) error {
s.lock.Lock()
defer s.lock.Unlock()
srcid := cmd.ID.String()
dstid := cmd.Config.ProcessID().String()
if cmd.Config.LimitCPU <= 0 || cmd.Config.LimitMemory <= 0 {
return fmt.Errorf("the process with the ID '%s' must have limits defined", dstid)
}
p, ok := s.data.Process[srcid]
if !ok {
return fmt.Errorf("the process with the ID '%s' doesn't exists", srcid)
}
if p.Config.Equal(cmd.Config) {
return nil
}
if srcid == dstid {
p.UpdatedAt = time.Now()
p.Config = cmd.Config
s.data.Process[srcid] = p
return nil
}
_, ok = s.data.Process[dstid]
if ok {
return fmt.Errorf("the process with the ID '%s' already exists", dstid)
}
now := time.Now()
p.CreatedAt = now
p.UpdatedAt = now
p.Config = cmd.Config
delete(s.data.Process, srcid)
s.data.Process[dstid] = p
return nil
}
func (s *store) setProcessOrder(cmd CommandSetProcessOrder) error {
s.lock.Lock()
defer s.lock.Unlock()
id := cmd.ID.String()
p, ok := s.data.Process[id]
if !ok {
return fmt.Errorf("the process with the ID '%s' doesn't exists", cmd.ID)
}
p.Order = cmd.Order
p.UpdatedAt = time.Now()
s.data.Process[id] = p
return nil
}
func (s *store) setProcessMetadata(cmd CommandSetProcessMetadata) error {
s.lock.Lock()
defer s.lock.Unlock()
id := cmd.ID.String()
p, ok := s.data.Process[id]
if !ok {
return fmt.Errorf("the process with the ID '%s' doesn't exists", cmd.ID)
}
if p.Metadata == nil {
p.Metadata = map[string]interface{}{}
}
if cmd.Data == nil {
delete(p.Metadata, cmd.Key)
} else {
p.Metadata[cmd.Key] = cmd.Data
}
p.UpdatedAt = time.Now()
s.data.Process[id] = p
return nil
}
func (s *store) setProcessError(cmd CommandSetProcessError) error {
s.lock.Lock()
defer s.lock.Unlock()
id := cmd.ID.String()
p, ok := s.data.Process[id]
if !ok {
return fmt.Errorf("the process with the ID '%s' doesn't exists", cmd.ID)
}
p.Error = cmd.Error
s.data.Process[id] = p
return nil
}
func (s *store) addIdentity(cmd CommandAddIdentity) error {
s.lock.Lock()
defer s.lock.Unlock()
if cmd.Identity.Name == "$anon" {
return fmt.Errorf("the identity with the name '%s' can't be created", cmd.Identity.Name)
}
_, ok := s.data.Users.Users[cmd.Identity.Name]
if ok {
return fmt.Errorf("the identity with the name '%s' already exists", cmd.Identity.Name)
}
s.data.Users.UpdatedAt = time.Now()
s.data.Users.Users[cmd.Identity.Name] = cmd.Identity
return nil
}
func (s *store) updateIdentity(cmd CommandUpdateIdentity) error {
s.lock.Lock()
defer s.lock.Unlock()
if cmd.Name == "$anon" {
return fmt.Errorf("the identity with the name '%s' can't be updated", cmd.Name)
}
_, ok := s.data.Users.Users[cmd.Name]
if !ok {
return fmt.Errorf("the identity with the name '%s' doesn't exist", cmd.Name)
}
if cmd.Name == cmd.Identity.Name {
s.data.Users.UpdatedAt = time.Now()
s.data.Users.Users[cmd.Identity.Name] = cmd.Identity
return nil
}
_, ok = s.data.Users.Users[cmd.Identity.Name]
if ok {
return fmt.Errorf("the identity with the name '%s' already exists", cmd.Identity.Name)
}
now := time.Now()
s.data.Users.UpdatedAt = now
s.data.Users.Users[cmd.Identity.Name] = cmd.Identity
s.data.Policies.UpdatedAt = now
s.data.Policies.Policies[cmd.Identity.Name] = s.data.Policies.Policies[cmd.Name]
delete(s.data.Users.Users, cmd.Name)
delete(s.data.Policies.Policies, cmd.Name)
return nil
}
func (s *store) removeIdentity(cmd CommandRemoveIdentity) error {
s.lock.Lock()
defer s.lock.Unlock()
delete(s.data.Users.Users, cmd.Name)
s.data.Users.UpdatedAt = time.Now()
delete(s.data.Policies.Policies, cmd.Name)
s.data.Policies.UpdatedAt = time.Now()
return nil
}
func (s *store) setPolicies(cmd CommandSetPolicies) error {
s.lock.Lock()
defer s.lock.Unlock()
if cmd.Name != "$anon" {
if _, ok := s.data.Users.Users[cmd.Name]; !ok {
return fmt.Errorf("the identity with the name '%s' doesn't exist", cmd.Name)
}
}
for i, p := range cmd.Policies {
if len(p.Domain) != 0 {
continue
}
p.Domain = "$none"
cmd.Policies[i] = p
}
delete(s.data.Policies.Policies, cmd.Name)
s.data.Policies.Policies[cmd.Name] = cmd.Policies
s.data.Policies.UpdatedAt = time.Now()
return nil
}
func (s *store) setProcessNodeMap(cmd CommandSetProcessNodeMap) error {
s.lock.Lock()
defer s.lock.Unlock()
s.data.ProcessNodeMap = cmd.Map
return nil
}
func (s *store) createLock(cmd CommandCreateLock) error {
s.lock.Lock()
defer s.lock.Unlock()
validUntil, ok := s.data.Locks[cmd.Name]
if ok {
if time.Now().Before(validUntil) {
return fmt.Errorf("the lock with the ID '%s' already exists", cmd.Name)
}
}
s.data.Locks[cmd.Name] = cmd.ValidUntil
return nil
}
func (s *store) deleteLock(cmd CommandDeleteLock) error {
s.lock.Lock()
defer s.lock.Unlock()
if _, ok := s.data.Locks[cmd.Name]; !ok {
return nil
}
delete(s.data.Locks, cmd.Name)
return nil
}
func (s *store) clearLocks(cmd CommandClearLocks) error {
s.lock.Lock()
defer s.lock.Unlock()
for name, validUntil := range s.data.Locks {
if time.Now().Before(validUntil) {
// Lock is still valid
continue
}
delete(s.data.Locks, name)
}
return nil
}
func (s *store) setKV(cmd CommandSetKV) error {
s.lock.Lock()
defer s.lock.Unlock()
value := s.data.KVS[cmd.Key]
value.Value = cmd.Value
value.UpdatedAt = time.Now()
s.data.KVS[cmd.Key] = value
return nil
}
func (s *store) unsetKV(cmd CommandUnsetKV) error {
s.lock.Lock()
defer s.lock.Unlock()
if _, ok := s.data.KVS[cmd.Key]; !ok {
return fs.ErrNotExist
}
delete(s.data.KVS, cmd.Key)
return nil
}
func (s *store) OnApply(fn func(op Operation)) { func (s *store) OnApply(fn func(op Operation)) {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
@@ -794,6 +457,22 @@ func (s *store) Restore(snapshot io.ReadCloser) error {
data.Process[id] = p data.Process[id] = p
} }
now := time.Now()
for name, u := range data.Users.Users {
data.Users.userlist.Add(u)
if u.CreatedAt.IsZero() {
u.CreatedAt = now
}
if u.UpdatedAt.IsZero() {
u.UpdatedAt = now
}
data.Users.Users[name] = u
}
if data.Version == 0 { if data.Version == 0 {
data.Version = 1 data.Version = 1
} }
@@ -803,167 +482,6 @@ func (s *store) Restore(snapshot io.ReadCloser) error {
return nil return nil
} }
func (s *store) ListProcesses() []Process {
s.lock.RLock()
defer s.lock.RUnlock()
processes := []Process{}
for _, p := range s.data.Process {
processes = append(processes, Process{
CreatedAt: p.CreatedAt,
UpdatedAt: p.UpdatedAt,
Config: p.Config.Clone(),
Order: p.Order,
Metadata: p.Metadata,
Error: p.Error,
})
}
return processes
}
func (s *store) GetProcess(id app.ProcessID) (Process, error) {
s.lock.RLock()
defer s.lock.RUnlock()
process, ok := s.data.Process[id.String()]
if !ok {
return Process{}, fmt.Errorf("not found")
}
return Process{
CreatedAt: process.CreatedAt,
UpdatedAt: process.UpdatedAt,
Config: process.Config.Clone(),
Order: process.Order,
Metadata: process.Metadata,
Error: process.Error,
}, nil
}
func (s *store) ListUsers() Users {
s.lock.RLock()
defer s.lock.RUnlock()
u := Users{
UpdatedAt: s.data.Users.UpdatedAt,
}
for _, user := range s.data.Users.Users {
u.Users = append(u.Users, user)
}
return u
}
func (s *store) GetUser(name string) Users {
s.lock.RLock()
defer s.lock.RUnlock()
u := Users{
UpdatedAt: s.data.Users.UpdatedAt,
}
if user, ok := s.data.Users.Users[name]; ok {
u.Users = append(u.Users, user)
}
return u
}
func (s *store) ListPolicies() Policies {
s.lock.RLock()
defer s.lock.RUnlock()
p := Policies{
UpdatedAt: s.data.Policies.UpdatedAt,
}
for _, policies := range s.data.Policies.Policies {
p.Policies = append(p.Policies, policies...)
}
return p
}
func (s *store) ListUserPolicies(name string) Policies {
s.lock.RLock()
defer s.lock.RUnlock()
p := Policies{
UpdatedAt: s.data.Policies.UpdatedAt,
}
p.Policies = append(p.Policies, s.data.Policies.Policies[name]...)
return p
}
func (s *store) GetProcessNodeMap() map[string]string {
s.lock.RLock()
defer s.lock.RUnlock()
m := map[string]string{}
for key, value := range s.data.ProcessNodeMap {
m[key] = value
}
return m
}
func (s *store) HasLock(name string) bool {
s.lock.RLock()
defer s.lock.RUnlock()
_, ok := s.data.Locks[name]
return ok
}
func (s *store) ListLocks() map[string]time.Time {
s.lock.RLock()
defer s.lock.RUnlock()
m := map[string]time.Time{}
for key, value := range s.data.Locks {
m[key] = value
}
return m
}
func (s *store) ListKVS(prefix string) map[string]Value {
s.lock.RLock()
defer s.lock.RUnlock()
m := map[string]Value{}
for key, value := range s.data.KVS {
if !strings.HasPrefix(key, prefix) {
continue
}
m[key] = value
}
return m
}
func (s *store) GetFromKVS(key string) (Value, error) {
s.lock.RLock()
defer s.lock.RUnlock()
value, ok := s.data.KVS[key]
if !ok {
return Value{}, fs.ErrNotExist
}
return value, nil
}
type fsmSnapshot struct { type fsmSnapshot struct {
data []byte data []byte
} }

File diff suppressed because it is too large Load Diff

View File

@@ -5701,6 +5701,10 @@ const docTemplate = `{
"auth": { "auth": {
"$ref": "#/definitions/api.IAMUserAuth" "$ref": "#/definitions/api.IAMUserAuth"
}, },
"created_at": {
"type": "integer",
"format": "int64"
},
"name": { "name": {
"type": "string" "type": "string"
}, },
@@ -5712,6 +5716,10 @@ const docTemplate = `{
}, },
"superuser": { "superuser": {
"type": "boolean" "type": "boolean"
},
"updated_at": {
"type": "integer",
"format": "int64"
} }
} }
}, },

View File

@@ -5693,6 +5693,10 @@
"auth": { "auth": {
"$ref": "#/definitions/api.IAMUserAuth" "$ref": "#/definitions/api.IAMUserAuth"
}, },
"created_at": {
"type": "integer",
"format": "int64"
},
"name": { "name": {
"type": "string" "type": "string"
}, },
@@ -5704,6 +5708,10 @@
}, },
"superuser": { "superuser": {
"type": "boolean" "type": "boolean"
},
"updated_at": {
"type": "integer",
"format": "int64"
} }
} }
}, },

View File

@@ -777,6 +777,9 @@ definitions:
type: string type: string
auth: auth:
$ref: '#/definitions/api.IAMUserAuth' $ref: '#/definitions/api.IAMUserAuth'
created_at:
format: int64
type: integer
name: name:
type: string type: string
policies: policies:
@@ -785,6 +788,9 @@ definitions:
type: array type: array
superuser: superuser:
type: boolean type: boolean
updated_at:
format: int64
type: integer
type: object type: object
api.IAMUserAuth: api.IAMUserAuth:
properties: properties:

View File

@@ -1,11 +1,15 @@
package api package api
import ( import (
"time"
"github.com/datarhei/core/v16/iam/access" "github.com/datarhei/core/v16/iam/access"
"github.com/datarhei/core/v16/iam/identity" "github.com/datarhei/core/v16/iam/identity"
) )
type IAMUser struct { type IAMUser struct {
CreatedAt int64 `json:"created_at" format:"int64"`
UpdatedAt int64 `json:"updated_at" format:"int64"`
Name string `json:"name"` Name string `json:"name"`
Alias string `json:"alias"` Alias string `json:"alias"`
Superuser bool `json:"superuser"` Superuser bool `json:"superuser"`
@@ -14,6 +18,8 @@ type IAMUser struct {
} }
func (u *IAMUser) Marshal(user identity.User, policies []access.Policy) { func (u *IAMUser) Marshal(user identity.User, policies []access.Policy) {
u.CreatedAt = user.CreatedAt.Unix()
u.UpdatedAt = user.UpdatedAt.Unix()
u.Name = user.Name u.Name = user.Name
u.Alias = user.Alias u.Alias = user.Alias
u.Superuser = user.Superuser u.Superuser = user.Superuser
@@ -47,6 +53,8 @@ func (u *IAMUser) Marshal(user identity.User, policies []access.Policy) {
func (u *IAMUser) Unmarshal() (identity.User, []access.Policy) { func (u *IAMUser) Unmarshal() (identity.User, []access.Policy) {
iamuser := identity.User{ iamuser := identity.User{
CreatedAt: time.Unix(u.CreatedAt, 0),
UpdatedAt: time.Unix(u.UpdatedAt, 0),
Name: u.Name, Name: u.Name,
Alias: u.Alias, Alias: u.Alias,
Superuser: u.Superuser, Superuser: u.Superuser,

View File

@@ -215,10 +215,24 @@ func (i *iam) AddPolicy(name, domain, resource string, actions []string) error {
domain = "$none" domain = "$none"
} }
if name != "$anon" {
if user, err := i.im.Get(name); err == nil {
// Update the "updatedAt" field
i.im.Update(name, user)
}
}
return i.am.AddPolicy(name, domain, resource, actions) return i.am.AddPolicy(name, domain, resource, actions)
} }
func (i *iam) RemovePolicy(name, domain, resource string, actions []string) error { func (i *iam) RemovePolicy(name, domain, resource string, actions []string) error {
if len(name) != 0 && name != "$anon" {
if user, err := i.im.Get(name); err == nil {
// Update the "updatedAt" field
i.im.Update(name, user)
}
}
return i.am.RemovePolicy(name, domain, resource, actions) return i.am.RemovePolicy(name, domain, resource, actions)
} }

View File

@@ -3,14 +3,10 @@ package identity
import ( import (
"fmt" "fmt"
"net/url" "net/url"
"strings"
"sync" "sync"
"time" "time"
enctoken "github.com/datarhei/core/v16/encoding/token" enctoken "github.com/datarhei/core/v16/encoding/token"
"github.com/datarhei/core/v16/log"
"github.com/datarhei/core/v16/slices"
"github.com/google/uuid"
jwtgo "github.com/golang-jwt/jwt/v5" jwtgo "github.com/golang-jwt/jwt/v5"
) )
@@ -20,79 +16,6 @@ import (
// the same Auth0.User can't have multiple identities // the same Auth0.User can't have multiple identities
// the whole jwks will be part of this package // the whole jwks will be part of this package
type User struct {
Name string `json:"name"`
Alias string `json:"alias"`
Superuser bool `json:"superuser"`
Auth UserAuth `json:"auth"`
}
type UserAuth struct {
API UserAuthAPI `json:"api"`
Services UserAuthServices `json:"services"`
}
type UserAuthAPI struct {
Password string `json:"password"`
Auth0 UserAuthAPIAuth0 `json:"auth0"`
}
type UserAuthAPIAuth0 struct {
User string `json:"user"`
Tenant Auth0Tenant `json:"tenant"`
}
type UserAuthServices struct {
Basic []string `json:"basic"` // Passwords for BasicAuth
Token []string `json:"token"` // Tokens/Streamkey for RTMP and SRT
Session []string `json:"session"` // Secrets for session JWT
}
func (u *User) Validate() error {
if len(u.Name) == 0 {
return fmt.Errorf("a name is required")
}
if strings.HasPrefix(u.Name, "$") {
return fmt.Errorf("name is not allowed to start with $")
}
if len(u.Alias) != 0 {
if strings.HasPrefix(u.Alias, "$") {
return fmt.Errorf("alias is not allowed to start with $")
}
}
if len(u.Auth.API.Auth0.User) != 0 {
t, err := newAuth0Tenant(u.Auth.API.Auth0.Tenant)
if err != nil {
return fmt.Errorf("auth0: %w", err)
}
t.Cancel()
}
return nil
}
func (u *User) marshalIdentity() *identity {
i := &identity{
user: *u,
}
return i
}
func (u *User) clone() User {
user := *u
user.Auth.Services.Basic = slices.Copy(u.Auth.Services.Basic)
user.Auth.Services.Token = slices.Copy(u.Auth.Services.Token)
user.Auth.Services.Session = slices.Copy(u.Auth.Services.Session)
return user
}
type Verifier interface { type Verifier interface {
Name() string // Name returns the name of the identity. Name() string // Name returns the name of the identity.
Alias() string // Alias returns the alias of the identity, or an empty string if no alias has been set. Alias() string // Alias returns the alias of the identity, or an empty string if no alias has been set.
@@ -484,554 +407,3 @@ func (i *identity) IsSuperuser() bool {
return i.user.Superuser return i.user.Superuser
} }
type Manager interface {
Create(identity User) error
Update(name string, identity User) error
Delete(name string) error
Get(name string) (User, error)
GetVerifier(name string) (Verifier, error)
GetVerifierFromAuth0(name string) (Verifier, error)
GetDefaultVerifier() (Verifier, error)
Reload() error // Reload users from adapter
Save() error // Save users to adapter
List() []User // List all users
Validators() []string
CreateJWT(name string) (string, string, error)
Close()
}
type identityManager struct {
root *identity
identities map[string]*identity
tenants map[string]*auth0Tenant
auth0UserIdentityMap map[string]string
adapter Adapter
autosave bool
logger log.Logger
jwtRealm string
jwtSecret []byte
lock sync.RWMutex
}
type Config struct {
Adapter Adapter
Superuser User
JWTRealm string
JWTSecret string
Logger log.Logger
}
func New(config Config) (Manager, error) {
im := &identityManager{
identities: map[string]*identity{},
tenants: map[string]*auth0Tenant{},
auth0UserIdentityMap: map[string]string{},
adapter: config.Adapter,
jwtRealm: config.JWTRealm,
jwtSecret: []byte(config.JWTSecret),
logger: config.Logger,
}
if im.logger == nil {
im.logger = log.New("")
}
if im.adapter == nil {
return nil, fmt.Errorf("no adapter provided")
}
config.Superuser.Superuser = true
identity, err := im.create(config.Superuser)
if err != nil {
return nil, err
}
im.root = identity
err = im.Reload()
if err != nil {
return nil, err
}
im.Save()
return im, nil
}
func (im *identityManager) Close() {
im.lock.Lock()
defer im.lock.Unlock()
im.adapter = nil
im.auth0UserIdentityMap = map[string]string{}
im.identities = map[string]*identity{}
im.root = nil
for _, t := range im.tenants {
t.Cancel()
}
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 {
im.delete(name)
}
for _, name := range names {
im.delete(name)
}
for _, u := range users {
if ok, _ := im.isNameAvailable(u.Name, u.Alias); !ok {
continue
}
if err := u.Validate(); err != nil {
return fmt.Errorf("invalid user from adapter: %s, %w", u.Name, err)
}
identity, err := im.create(u)
if err != nil {
continue
}
im.identities[identity.user.Name] = identity
if len(identity.user.Alias) != 0 {
im.identities[identity.user.Alias] = identity
}
}
return nil
}
func (im *identityManager) isNameAvailable(name, alias string) (bool, error) {
if im.root == nil {
return true, nil
}
if name == im.root.user.Name {
return false, fmt.Errorf("name already exists")
}
if name == im.root.user.Alias {
return false, fmt.Errorf("name already exists")
}
if _, ok := im.identities[name]; ok {
return false, fmt.Errorf("name already exists")
}
if len(alias) != 0 {
if alias == im.root.user.Name {
return false, fmt.Errorf("alias already exists")
}
if alias == im.root.user.Alias {
return false, fmt.Errorf("alias already exists")
}
if _, ok := im.identities[alias]; ok {
return false, fmt.Errorf("alias already exists")
}
}
return true, nil
}
func (im *identityManager) Create(u User) error {
if err := u.Validate(); err != nil {
return err
}
im.lock.Lock()
defer im.lock.Unlock()
if ok, err := im.isNameAvailable(u.Name, u.Alias); !ok {
return err
}
identity, err := im.create(u)
if err != nil {
return err
}
im.identities[identity.user.Name] = identity
if len(identity.user.Alias) != 0 {
im.identities[identity.user.Alias] = identity
}
if im.autosave {
im.save()
}
return nil
}
func (im *identityManager) create(u User) (*identity, error) {
u = u.clone()
identity := u.marshalIdentity()
if len(identity.user.Auth.API.Auth0.User) != 0 {
if _, ok := im.auth0UserIdentityMap[identity.user.Auth.API.Auth0.User]; ok {
return nil, fmt.Errorf("the Auth0 user has already an identity")
}
auth0Key := identity.user.Auth.API.Auth0.Tenant.key()
if tenant, ok := im.tenants[auth0Key]; !ok {
tenant, err := newAuth0Tenant(identity.user.Auth.API.Auth0.Tenant)
if err != nil {
return nil, err
}
im.tenants[auth0Key] = tenant
identity.tenant = tenant
} else {
tenant.AddClientID(identity.user.Auth.API.Auth0.Tenant.ClientID)
identity.tenant = tenant
}
im.auth0UserIdentityMap[identity.user.Auth.API.Auth0.User] = u.Name
}
identity.valid = true
im.logger.Debug().WithField("name", identity.Name()).Log("Identity created")
return identity, nil
}
func (im *identityManager) Update(name string, u User) error {
if err := u.Validate(); err != nil {
return err
}
im.lock.Lock()
defer im.lock.Unlock()
if im.root.user.Name == name || im.root.user.Alias == name {
return fmt.Errorf("this identity cannot be updated")
}
oldidentity, ok := im.identities[name]
if !ok {
return fmt.Errorf("identity not found")
}
delete(im.identities, oldidentity.user.Name)
delete(im.identities, oldidentity.user.Alias)
ok, err := im.isNameAvailable(u.Name, u.Alias)
im.identities[oldidentity.user.Name] = oldidentity
if len(oldidentity.user.Alias) != 0 {
im.identities[oldidentity.user.Alias] = oldidentity
}
if !ok {
return err
}
err = im.delete(name)
if err != nil {
return err
}
identity, err := im.create(u)
if err != nil {
// restore old identity
im.create(oldidentity.user)
im.identities[oldidentity.user.Name] = oldidentity
if len(oldidentity.user.Alias) != 0 {
im.identities[oldidentity.user.Alias] = oldidentity
}
return err
}
im.identities[identity.user.Name] = identity
if len(identity.user.Alias) != 0 {
im.identities[identity.user.Alias] = identity
}
im.logger.Debug().WithFields(log.Fields{
"oldname": name,
"newname": identity.Name(),
}).Log("Identity updated")
if im.autosave {
im.save()
}
return nil
}
func (im *identityManager) Delete(name string) error {
im.lock.Lock()
defer im.lock.Unlock()
err := im.delete(name)
if err != nil {
return err
}
return nil
}
func (im *identityManager) delete(name string) error {
if im.root.user.Name == name || im.root.user.Alias == name {
return fmt.Errorf("this identity can't be removed")
}
identity, ok := im.identities[name]
if !ok {
return fmt.Errorf("identity not found")
}
delete(im.identities, identity.user.Name)
delete(im.identities, identity.user.Alias)
identity.lock.Lock()
identity.valid = false
identity.lock.Unlock()
if len(identity.user.Auth.API.Auth0.User) == 0 {
if im.autosave {
im.save()
}
return nil
}
delete(im.auth0UserIdentityMap, identity.user.Auth.API.Auth0.User)
// find out if the tenant is still used somewhere else
found := false
for _, i := range im.identities {
if i.tenant == identity.tenant {
found = true
break
}
}
if !found {
identity.tenant.Cancel()
delete(im.tenants, identity.user.Auth.API.Auth0.Tenant.key())
if im.autosave {
im.save()
}
return nil
}
// find out if the tenant's clientid is still used somewhere else
found = false
for _, i := range im.identities {
if len(i.user.Auth.API.Auth0.User) == 0 {
continue
}
if i.user.Auth.API.Auth0.Tenant.ClientID == identity.user.Auth.API.Auth0.Tenant.ClientID {
found = true
break
}
}
if !found {
identity.tenant.RemoveClientID(identity.user.Auth.API.Auth0.Tenant.ClientID)
}
if im.autosave {
if err := im.save(); err != nil {
return err
}
}
return nil
}
func (im *identityManager) getIdentity(name string) (*identity, error) {
var identity *identity = nil
if im.root.user.Name == name || im.root.user.Alias == name {
identity = im.root
} else {
identity = im.identities[name]
}
if identity == nil {
return nil, fmt.Errorf("identity not found")
}
identity.jwtRealm = im.jwtRealm
identity.jwtKeyFunc = func(*jwtgo.Token) (interface{}, error) { return im.jwtSecret, nil }
return identity, nil
}
func (im *identityManager) Get(name string) (User, error) {
im.lock.RLock()
defer im.lock.RUnlock()
identity, err := im.getIdentity(name)
if err != nil {
return User{}, err
}
user := identity.user.clone()
return user, nil
}
func (im *identityManager) GetVerifier(name string) (Verifier, error) {
im.lock.RLock()
defer im.lock.RUnlock()
return im.getIdentity(name)
}
func (im *identityManager) GetVerifierFromAuth0(name string) (Verifier, error) {
im.lock.RLock()
defer im.lock.RUnlock()
name, ok := im.auth0UserIdentityMap[name]
if !ok {
return nil, fmt.Errorf("not found")
}
return im.getIdentity(name)
}
func (im *identityManager) GetDefaultVerifier() (Verifier, error) {
return im.root, nil
}
func (im *identityManager) List() []User {
im.lock.RLock()
defer im.lock.RUnlock()
users := []User{}
for _, identity := range im.identities {
users = append(users, identity.user.clone())
}
return users
}
func (im *identityManager) Save() error {
im.lock.RLock()
defer im.lock.RUnlock()
return im.save()
}
func (im *identityManager) save() error {
users := []User{}
for _, u := range im.identities {
users = append(users, u.user)
}
return im.adapter.SaveIdentities(users)
}
func (im *identityManager) Autosave(auto bool) {
im.lock.Lock()
defer im.lock.Unlock()
im.autosave = auto
}
func (im *identityManager) Validators() []string {
validators := []string{"localjwt"}
im.lock.RLock()
defer im.lock.RUnlock()
for _, t := range im.tenants {
for _, clientid := range t.clientIDs {
validators = append(validators, fmt.Sprintf("auth0 domain=%s audience=%s clientid=%s", t.domain, t.audience, clientid))
}
}
return validators
}
func (im *identityManager) CreateJWT(name string) (string, string, error) {
im.lock.RLock()
defer im.lock.RUnlock()
identity, err := im.getIdentity(name)
if err != nil {
return "", "", err
}
now := time.Now()
accessExpires := now.Add(time.Minute * 10)
refreshExpires := now.Add(time.Hour * 24)
// Create access token
accessToken := jwtgo.NewWithClaims(jwtgo.SigningMethodHS256, jwtgo.MapClaims{
"iss": im.jwtRealm,
"sub": identity.Name(),
"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(im.jwtSecret)
if err != nil {
return "", "", err
}
// Create refresh token
refreshToken := jwtgo.NewWithClaims(jwtgo.SigningMethodHS256, jwtgo.MapClaims{
"iss": im.jwtRealm,
"sub": identity.Name(),
"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(im.jwtSecret)
if err != nil {
return "", "", err
}
return at, rt, nil
}

View File

@@ -17,42 +17,6 @@ func createAdapter() (Adapter, error) {
return NewJSONAdapter(dummyfs, "./users.json", nil) return NewJSONAdapter(dummyfs, "./users.json", nil)
} }
func TestUserName(t *testing.T) {
user := User{}
err := user.Validate()
require.Error(t, err)
user.Name = "foobar_5"
err = user.Validate()
require.NoError(t, err)
user.Name = "foobar:5"
err = user.Validate()
require.NoError(t, err)
user.Name = "$foob:ar"
err = user.Validate()
require.Error(t, err)
}
func TestUserAlias(t *testing.T) {
user := User{
Name: "foober",
}
err := user.Validate()
require.NoError(t, err)
user.Alias = "foobar"
err = user.Validate()
require.NoError(t, err)
user.Alias = "$foob:ar"
err = user.Validate()
require.Error(t, err)
}
func TestIdentity(t *testing.T) { func TestIdentity(t *testing.T) {
user := User{ user := User{
Name: "foobar", Name: "foobar",
@@ -91,26 +55,6 @@ func TestIdentity(t *testing.T) {
require.Nil(t, id) require.Nil(t, id)
} }
func TestDefaultIdentity(t *testing.T) {
adapter, err := createAdapter()
require.NoError(t, err)
im, err := New(Config{
Adapter: adapter,
Superuser: User{Name: "foobar"},
JWTRealm: "test-realm",
JWTSecret: "abc123",
Logger: nil,
})
require.NoError(t, err)
require.NotNil(t, im)
identity, err := im.GetDefaultVerifier()
require.NoError(t, err)
require.NotNil(t, identity)
require.Equal(t, "foobar", identity.Name())
}
func TestIdentityAPIAuth(t *testing.T) { func TestIdentityAPIAuth(t *testing.T) {
user := User{ user := User{
Name: "foobar", Name: "foobar",
@@ -245,706 +189,3 @@ func TestIdentityServiceSessionAuth(t *testing.T) {
require.Equal(t, nil, data) require.Equal(t, nil, data)
require.NoError(t, err) require.NoError(t, err)
} }
func TestJWT(t *testing.T) {
adapter, err := createAdapter()
require.NoError(t, err)
im, err := New(Config{
Adapter: adapter,
Superuser: User{Name: "foobar"},
JWTRealm: "test-realm",
JWTSecret: "abc123",
Logger: nil,
})
require.NoError(t, err)
require.NotNil(t, im)
access, refresh, err := im.CreateJWT("foobaz")
require.Error(t, err)
require.Equal(t, "", access)
require.Equal(t, "", refresh)
access, refresh, err = im.CreateJWT("foobar")
require.NoError(t, err)
identity, err := im.GetVerifier("foobar")
require.NoError(t, err)
require.NotNil(t, identity)
ok, err := identity.VerifyJWT("something")
require.Error(t, err)
require.False(t, ok)
ok, err = identity.VerifyJWT(access)
require.NoError(t, err)
require.True(t, ok)
ok, err = identity.VerifyJWT(refresh)
require.NoError(t, err)
require.True(t, ok)
err = im.Create(User{Name: "foobaz"})
require.NoError(t, err)
access, refresh, err = im.CreateJWT("foobaz")
require.NoError(t, err)
ok, err = identity.VerifyJWT(access)
require.Error(t, err)
require.False(t, ok)
ok, err = identity.VerifyJWT(refresh)
require.Error(t, err)
require.False(t, ok)
}
func TestCreateUser(t *testing.T) {
adapter, err := createAdapter()
require.NoError(t, err)
im, err := New(Config{
Adapter: adapter,
Superuser: User{Name: "foobar", Alias: "foobalias"},
JWTRealm: "test-realm",
JWTSecret: "abc123",
Logger: nil,
})
require.NoError(t, err)
require.NotNil(t, im)
err = im.Create(User{Name: "foobar"})
require.Error(t, err)
err = im.Create(User{Name: "foobaz", Alias: "foobalias"})
require.Error(t, err)
err = im.Create(User{Name: "foobaz", Alias: "alias"})
require.NoError(t, err)
err = im.Create(User{Name: "fooboz", Alias: "alias"})
require.Error(t, err)
err = im.Create(User{Name: "foobaz", Alias: "somealias"})
require.Error(t, err)
}
func TestAlias(t *testing.T) {
adapter, err := createAdapter()
require.NoError(t, err)
im, err := New(Config{
Adapter: adapter,
Superuser: User{Name: "foobar", Alias: "foobalias"},
JWTRealm: "test-realm",
JWTSecret: "abc123",
Logger: nil,
})
require.NoError(t, err)
require.NotNil(t, im)
err = im.Create(User{Name: "foobaz", Alias: "alias"})
require.NoError(t, err)
identity, err := im.Get("foobar")
require.NoError(t, err)
require.Equal(t, "foobar", identity.Name)
require.Equal(t, "foobalias", identity.Alias)
identity, err = im.Get("foobalias")
require.NoError(t, err)
require.Equal(t, "foobar", identity.Name)
require.Equal(t, "foobalias", identity.Alias)
identity, err = im.Get("foobaz")
require.NoError(t, err)
require.Equal(t, "foobaz", identity.Name)
require.Equal(t, "alias", identity.Alias)
identity, err = im.Get("alias")
require.NoError(t, err)
require.Equal(t, "foobaz", identity.Name)
require.Equal(t, "alias", identity.Alias)
err = im.Create(User{Name: "alias", Alias: "foobaz"})
require.Error(t, err)
}
func TestCreateUserAuth0(t *testing.T) {
adapter, err := createAdapter()
require.NoError(t, err)
im, err := New(Config{
Adapter: adapter,
Superuser: User{Name: "foobar"},
JWTRealm: "test-realm",
JWTSecret: "abc123",
Logger: nil,
})
require.NoError(t, err)
require.NotNil(t, im)
require.ElementsMatch(t, []string{"localjwt"}, im.Validators())
err = im.Create(User{
Name: "foobaz",
Superuser: false,
Auth: UserAuth{
API: UserAuthAPI{
Auth0: UserAuthAPIAuth0{
User: "auth0|123456",
Tenant: Auth0Tenant{
Domain: "example.com",
Audience: "https://api.example.com/",
ClientID: "123456",
},
},
},
},
})
require.Error(t, err)
err = im.Create(User{
Name: "foobaz",
Superuser: false,
Auth: UserAuth{
API: UserAuthAPI{
Auth0: UserAuthAPIAuth0{
User: "auth0|123456",
Tenant: Auth0Tenant{
Domain: "datarhei-demo.eu.auth0.com",
Audience: "https://datarhei-demo.eu.auth0.com/api/v2/",
ClientID: "123456",
},
},
},
},
})
require.NoError(t, err)
identity, err := im.GetVerifierFromAuth0("foobaz")
require.Error(t, err)
require.Nil(t, identity)
identity, err = im.GetVerifierFromAuth0("auth0|123456")
require.NoError(t, err)
require.NotNil(t, identity)
manager, ok := im.(*identityManager)
require.True(t, ok)
require.NotNil(t, manager)
require.Equal(t, 1, len(manager.tenants))
require.Equal(t, map[string]string{"auth0|123456": "foobaz"}, manager.auth0UserIdentityMap)
require.ElementsMatch(t, []string{
"localjwt",
"auth0 domain=datarhei-demo.eu.auth0.com audience=https://datarhei-demo.eu.auth0.com/api/v2/ clientid=123456",
}, im.Validators())
err = im.Create(User{
Name: "fooboz",
Superuser: false,
Auth: UserAuth{
API: UserAuthAPI{
Auth0: UserAuthAPIAuth0{
User: "auth0|123456",
Tenant: Auth0Tenant{
Domain: "datarhei-demo.eu.auth0.com",
Audience: "https://datarhei-demo.eu.auth0.com/api/v2/",
ClientID: "123456",
},
},
},
},
})
require.Error(t, err)
err = im.Create(User{
Name: "fooboz",
Superuser: false,
Auth: UserAuth{
API: UserAuthAPI{
Auth0: UserAuthAPIAuth0{
User: "auth0|987654",
Tenant: Auth0Tenant{
Domain: "datarhei-demo.eu.auth0.com",
Audience: "https://datarhei-demo.eu.auth0.com/api/v2/",
ClientID: "987654",
},
},
},
},
})
require.NoError(t, err)
require.Equal(t, 1, len(manager.tenants))
require.Equal(t, map[string]string{"auth0|123456": "foobaz", "auth0|987654": "fooboz"}, manager.auth0UserIdentityMap)
require.ElementsMatch(t, []string{
"localjwt",
"auth0 domain=datarhei-demo.eu.auth0.com audience=https://datarhei-demo.eu.auth0.com/api/v2/ clientid=123456",
"auth0 domain=datarhei-demo.eu.auth0.com audience=https://datarhei-demo.eu.auth0.com/api/v2/ clientid=987654",
}, im.Validators())
im.Close()
}
func TestLoadAndSave(t *testing.T) {
adptr, err := createAdapter()
require.NoError(t, err)
dummyfs := adptr.(*fileAdapter).fs
im, err := New(Config{
Adapter: adptr,
Superuser: User{Name: "foobar"},
JWTRealm: "test-realm",
JWTSecret: "abc123",
Logger: nil,
})
require.NoError(t, err)
require.NotNil(t, im)
err = im.Save()
require.NoError(t, err)
_, err = dummyfs.Stat("./users.json")
require.NoError(t, err)
data, err := dummyfs.ReadFile("./users.json")
require.NoError(t, err)
require.Equal(t, []byte("[]"), data)
err = im.Create(User{Name: "foobaz", Alias: "alias"})
require.NoError(t, err)
identity, err := im.GetVerifier("foobaz")
require.NoError(t, err)
require.NotNil(t, identity)
identity, err = im.GetVerifier("alias")
require.NoError(t, err)
require.NotNil(t, identity)
err = im.Save()
require.NoError(t, err)
im, err = New(Config{
Adapter: adptr,
Superuser: User{Name: "foobar"},
JWTRealm: "test-realm",
JWTSecret: "abc123",
Logger: nil,
})
require.NoError(t, err)
require.NotNil(t, im)
identity, err = im.GetVerifier("foobaz")
require.NoError(t, err)
require.NotNil(t, identity)
identity, err = im.GetVerifier("alias")
require.NoError(t, err)
require.NotNil(t, identity)
}
func TestUpdateUser(t *testing.T) {
adapter, err := createAdapter()
require.NoError(t, err)
im, err := New(Config{
Adapter: adapter,
Superuser: User{Name: "foobar"},
JWTRealm: "test-realm",
JWTSecret: "abc123",
Logger: nil,
})
require.NoError(t, err)
require.NotNil(t, im)
err = im.Create(User{Name: "fooboz"})
require.NoError(t, err)
err = im.Update("unknown", User{Name: "fooboz"})
require.Error(t, err)
err = im.Update("foobar", User{Name: "foobar"})
require.Error(t, err)
err = im.Update("foobar", User{Name: "fooboz"})
require.Error(t, err)
identity, err := im.GetVerifier("foobar")
require.NoError(t, err)
require.NotNil(t, identity)
require.Equal(t, "foobar", identity.Name())
err = im.Update("foobar", User{Name: "foobaz"})
require.Error(t, err)
require.Equal(t, "foobar", identity.Name())
identity, err = im.GetVerifier("foobaz")
require.Error(t, err)
require.Nil(t, identity)
identity, err = im.GetVerifier("fooboz")
require.NoError(t, err)
require.NotNil(t, identity)
require.Equal(t, "fooboz", identity.Name())
err = im.Update("fooboz", User{Name: "foobaz"})
require.NoError(t, err)
}
func TestUpdateUserAlias(t *testing.T) {
adapter, err := createAdapter()
require.NoError(t, err)
im, err := New(Config{
Adapter: adapter,
Superuser: User{Name: "foobar", Alias: "superalias"},
JWTRealm: "test-realm",
JWTSecret: "abc123",
Logger: nil,
})
require.NoError(t, err)
require.NotNil(t, im)
err = im.Create(User{Name: "fooboz"})
require.NoError(t, err)
identity, err := im.GetVerifier("fooboz")
require.NoError(t, err)
require.NotNil(t, identity)
require.Equal(t, "fooboz", identity.Name())
_, err = im.GetVerifier("alias")
require.Error(t, err)
err = im.Update("fooboz", User{Name: "fooboz", Alias: "alias"})
require.NoError(t, err)
identity, err = im.GetVerifier("fooboz")
require.NoError(t, err)
require.NotNil(t, identity)
require.Equal(t, "fooboz", identity.Name())
require.Equal(t, "alias", identity.Alias())
err = im.Create(User{Name: "barfoo", Alias: "alias2"})
require.NoError(t, err)
err = im.Update("fooboz", User{Name: "fooboz", Alias: "alias2"})
require.Error(t, err)
err = im.Update("fooboz", User{Name: "barfoo", Alias: "alias"})
require.Error(t, err)
err = im.Update("fooboz", User{Name: "fooboz", Alias: "superalias"})
require.Error(t, err)
err = im.Update("fooboz", User{Name: "foobar", Alias: ""})
require.Error(t, err)
}
func TestUpdateUserAuth0(t *testing.T) {
adapter, err := createAdapter()
require.NoError(t, err)
im, err := New(Config{
Adapter: adapter,
Superuser: User{Name: "foobar"},
JWTRealm: "test-realm",
JWTSecret: "abc123",
Logger: nil,
})
require.NoError(t, err)
require.NotNil(t, im)
err = im.Create(User{
Name: "foobaz",
Superuser: false,
Auth: UserAuth{
API: UserAuthAPI{
Auth0: UserAuthAPIAuth0{
User: "auth0|123456",
Tenant: Auth0Tenant{
Domain: "datarhei-demo.eu.auth0.com",
Audience: "https://datarhei-demo.eu.auth0.com/api/v2/",
ClientID: "123456",
},
},
},
},
})
require.NoError(t, err)
identity, err := im.GetVerifierFromAuth0("auth0|123456")
require.NoError(t, err)
require.NotNil(t, identity)
identity, err = im.GetVerifier("foobaz")
require.NoError(t, err)
require.NotNil(t, identity)
user, err := im.Get("foobaz")
require.NoError(t, err)
user.Name = "fooboz"
err = im.Update("foobaz", user)
require.NoError(t, err)
identity, err = im.GetVerifierFromAuth0("auth0|123456")
require.NoError(t, err)
require.NotNil(t, identity)
identity, err = im.GetVerifier("fooboz")
require.NoError(t, err)
require.NotNil(t, identity)
}
func TestRemoveUser(t *testing.T) {
adapter, err := createAdapter()
require.NoError(t, err)
im, err := New(Config{
Adapter: adapter,
Superuser: User{Name: "foobar"},
JWTRealm: "test-realm",
JWTSecret: "abc123",
Logger: nil,
})
require.NoError(t, err)
require.NotNil(t, im)
err = im.Delete("fooboz")
require.Error(t, err)
err = im.Delete("foobar")
require.Error(t, err)
err = im.Create(User{
Name: "foobaz",
Superuser: false,
Auth: UserAuth{
API: UserAuthAPI{
Password: "apisecret",
Auth0: UserAuthAPIAuth0{},
},
Services: UserAuthServices{
Basic: []string{"secret"},
Token: []string{"tokensecret"},
Session: []string{"sessionsecret"},
},
},
})
require.NoError(t, err)
identity, err := im.GetVerifier("foobaz")
require.NoError(t, err)
require.NotNil(t, identity)
ok, err := identity.VerifyAPIPassword("apisecret")
require.True(t, ok)
require.NoError(t, err)
ok, err = identity.VerifyServiceBasicAuth("secret")
require.True(t, ok)
require.NoError(t, err)
ok, err = identity.VerifyServiceToken("tokensecret")
require.True(t, ok)
require.NoError(t, err)
session := identity.GetServiceSession(nil, time.Hour)
ok, data, err := identity.VerifyServiceSession(session)
require.True(t, ok)
require.Equal(t, nil, data)
require.NoError(t, err)
access, refresh, err := im.CreateJWT("foobaz")
require.NoError(t, err)
ok, err = identity.VerifyJWT(access)
require.True(t, ok)
require.NoError(t, err)
ok, err = identity.VerifyJWT(refresh)
require.True(t, ok)
require.NoError(t, err)
err = im.Delete("foobaz")
require.NoError(t, err)
ok, err = identity.VerifyAPIPassword("apisecret")
require.False(t, ok)
require.Error(t, err)
ok, err = identity.VerifyServiceBasicAuth("secret")
require.False(t, ok)
require.Error(t, err)
ok, err = identity.VerifyServiceToken("tokensecret")
require.False(t, ok)
require.Error(t, err)
ok, data, err = identity.VerifyServiceSession(session)
require.False(t, ok)
require.Equal(t, nil, data)
require.Error(t, err)
ok, err = identity.VerifyJWT(access)
require.False(t, ok)
require.Error(t, err)
ok, err = identity.VerifyJWT(refresh)
require.False(t, ok)
require.Error(t, err)
identity, err = im.GetVerifier("foobaz")
require.Error(t, err)
require.Nil(t, identity)
access, refresh, err = im.CreateJWT("foobaz")
require.Error(t, err)
require.Empty(t, access)
require.Empty(t, refresh)
}
func TestRemoveUserAuth0(t *testing.T) {
adapter, err := createAdapter()
require.NoError(t, err)
im, err := New(Config{
Adapter: adapter,
Superuser: User{Name: "foobar"},
JWTRealm: "test-realm",
JWTSecret: "abc123",
Logger: nil,
})
require.NoError(t, err)
require.NotNil(t, im)
err = im.Create(User{
Name: "foobaz",
Superuser: false,
Auth: UserAuth{
API: UserAuthAPI{
Auth0: UserAuthAPIAuth0{
User: "auth0|123456",
Tenant: Auth0Tenant{
Domain: "datarhei-demo.eu.auth0.com",
Audience: "https://datarhei-demo.eu.auth0.com/api/v2/",
ClientID: "123456",
},
},
},
},
})
require.NoError(t, err)
err = im.Create(User{
Name: "fooboz",
Superuser: false,
Auth: UserAuth{
API: UserAuthAPI{
Auth0: UserAuthAPIAuth0{
User: "auth0|987654",
Tenant: Auth0Tenant{
Domain: "datarhei-demo.eu.auth0.com",
Audience: "https://datarhei-demo.eu.auth0.com/api/v2/",
ClientID: "987654",
},
},
},
},
})
require.NoError(t, err)
manager, ok := im.(*identityManager)
require.True(t, ok)
require.NotNil(t, manager)
require.Equal(t, 1, len(manager.tenants))
require.Equal(t, map[string]string{"auth0|123456": "foobaz", "auth0|987654": "fooboz"}, manager.auth0UserIdentityMap)
require.ElementsMatch(t, []string{
"localjwt",
"auth0 domain=datarhei-demo.eu.auth0.com audience=https://datarhei-demo.eu.auth0.com/api/v2/ clientid=123456",
"auth0 domain=datarhei-demo.eu.auth0.com audience=https://datarhei-demo.eu.auth0.com/api/v2/ clientid=987654",
}, im.Validators())
err = im.Delete("foobaz")
require.NoError(t, err)
require.Equal(t, 1, len(manager.tenants))
require.Equal(t, map[string]string{"auth0|987654": "fooboz"}, manager.auth0UserIdentityMap)
require.ElementsMatch(t, []string{
"localjwt",
"auth0 domain=datarhei-demo.eu.auth0.com audience=https://datarhei-demo.eu.auth0.com/api/v2/ clientid=987654",
}, im.Validators())
err = im.Delete("fooboz")
require.NoError(t, err)
require.Equal(t, 0, len(manager.tenants))
require.ElementsMatch(t, []string{
"localjwt",
}, im.Validators())
}
func TestAutosave(t *testing.T) {
adptr, err := createAdapter()
require.NoError(t, err)
dummyfs := adptr.(*fileAdapter).fs
im, err := New(Config{
Adapter: adptr,
Superuser: User{Name: "foobar"},
JWTRealm: "test-realm",
JWTSecret: "abc123",
Logger: nil,
})
require.NoError(t, err)
require.NotNil(t, im)
err = im.Save()
require.NoError(t, err)
_, err = dummyfs.Stat("./users.json")
require.NoError(t, err)
data, err := dummyfs.ReadFile("./users.json")
require.NoError(t, err)
require.Equal(t, []byte("[]"), data)
err = im.Create(User{Name: "foobaz"})
require.NoError(t, err)
data, err = dummyfs.ReadFile("./users.json")
require.NoError(t, err)
require.NotEqual(t, []byte("[]"), data)
user, err := im.Get("foobaz")
require.NoError(t, err)
user.Name = "fooboz"
err = im.Update("foobaz", user)
require.NoError(t, err)
data, err = dummyfs.ReadFile("./users.json")
require.NoError(t, err)
require.NotEqual(t, []byte("[]"), data)
err = im.Delete("fooboz")
require.NoError(t, err)
data, err = dummyfs.ReadFile("./users.json")
require.NoError(t, err)
require.Equal(t, []byte("[]"), data)
}

536
iam/identity/manager.go Normal file
View File

@@ -0,0 +1,536 @@
package identity
import (
"fmt"
"sync"
"time"
"github.com/datarhei/core/v16/log"
jwtgo "github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
)
type Manager interface {
Create(identity User) error
Update(name string, identity User) error
Delete(name string) error
Get(name string) (User, error)
GetVerifier(name string) (Verifier, error)
GetVerifierFromAuth0(name string) (Verifier, error)
GetDefaultVerifier() (Verifier, error)
Reload() error // Reload users from adapter
Save() error // Save users to adapter
List() []User // List all users
Validators() []string
CreateJWT(name string) (string, string, error)
Close()
}
type identityManager struct {
root *identity
userlist UserList
identities map[string]*identity
tenants map[string]*auth0Tenant
auth0UserIdentityMap map[string]string
adapter Adapter
autosave bool
logger log.Logger
jwtRealm string
jwtSecret []byte
lock sync.RWMutex
}
type Config struct {
Adapter Adapter
Superuser User
JWTRealm string
JWTSecret string
Logger log.Logger
}
func New(config Config) (Manager, error) {
im := &identityManager{
userlist: NewUserList(),
identities: map[string]*identity{},
tenants: map[string]*auth0Tenant{},
auth0UserIdentityMap: map[string]string{},
adapter: config.Adapter,
jwtRealm: config.JWTRealm,
jwtSecret: []byte(config.JWTSecret),
logger: config.Logger,
}
if im.logger == nil {
im.logger = log.New("")
}
if im.adapter == nil {
return nil, fmt.Errorf("no adapter provided")
}
config.Superuser.Superuser = true
identity, err := im.createIdentity(config.Superuser)
if err != nil {
return nil, err
}
im.root = identity
err = im.Reload()
if err != nil {
return nil, err
}
im.Save()
return im, nil
}
func (im *identityManager) Close() {
im.lock.Lock()
defer im.lock.Unlock()
im.adapter = nil
im.userlist = nil
im.identities = map[string]*identity{}
im.root = nil
for _, t := range im.tenants {
t.Cancel()
}
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)
}
userlist := NewUserList()
now := time.Now()
for _, u := range users {
if u.CreatedAt.IsZero() {
u.CreatedAt = now
}
if u.UpdatedAt.IsZero() {
u.UpdatedAt = now
}
err := userlist.Add(u)
if err != nil {
return fmt.Errorf("invalid user %s from adapter: %w", u.Name, err)
}
}
userlist.Delete(im.root.user.Name)
userlist.Delete(im.root.user.Alias)
im.lock.Lock()
defer im.lock.Unlock()
im.autosave = false
defer func() {
im.autosave = true
}()
for name := range im.identities {
im.delete(name)
}
im.userlist = userlist
for _, u := range userlist.List() {
identity, err := im.createIdentity(u)
if err != nil {
continue
}
im.identities[u.Name] = identity
}
return nil
}
func (im *identityManager) Create(u User) error {
im.lock.Lock()
defer im.lock.Unlock()
if im.root.user.Name == u.Name || im.root.user.Alias == u.Name {
return fmt.Errorf("the identity %s already exists", u.Name)
}
if len(u.Alias) != 0 {
if im.root.user.Name == u.Alias || im.root.user.Alias == u.Alias {
return fmt.Errorf("the identity %s already exists", u.Alias)
}
}
now := time.Now()
u.CreatedAt = now
u.UpdatedAt = now
err := im.userlist.Add(u)
if err != nil {
return err
}
identity, err := im.createIdentity(u)
if err != nil {
return err
}
im.identities[identity.user.Name] = identity
if im.autosave {
im.save()
}
return nil
}
func (im *identityManager) createIdentity(u User) (*identity, error) {
u = u.clone()
identity := u.marshalIdentity()
if len(identity.user.Auth.API.Auth0.User) != 0 {
auth0Key := identity.user.Auth.API.Auth0.Tenant.key()
if tenant, ok := im.tenants[auth0Key]; !ok {
tenant, err := newAuth0Tenant(identity.user.Auth.API.Auth0.Tenant)
if err != nil {
return nil, err
}
im.tenants[auth0Key] = tenant
identity.tenant = tenant
} else {
tenant.AddClientID(identity.user.Auth.API.Auth0.Tenant.ClientID)
identity.tenant = tenant
}
im.auth0UserIdentityMap[identity.user.Auth.API.Auth0.User] = identity.user.Name
}
identity.valid = true
im.logger.Debug().WithField("name", identity.Name()).Log("Identity created")
return identity, nil
}
func (im *identityManager) Update(nameOrAlias string, u User) error {
im.lock.Lock()
defer im.lock.Unlock()
if im.root.user.Name == nameOrAlias || im.root.user.Alias == nameOrAlias {
return fmt.Errorf("the identity %s cannot be updated", nameOrAlias)
}
if im.root.user.Name == u.Name || im.root.user.Alias == u.Name {
return fmt.Errorf("the identity %s already exists", u.Name)
}
if len(u.Alias) != 0 {
if im.root.user.Name == u.Alias || im.root.user.Alias == u.Alias {
return fmt.Errorf("the identity %s already exists", u.Alias)
}
}
oldUser, err := im.userlist.Get(nameOrAlias)
if err != nil {
return err
}
u.CreatedAt = oldUser.CreatedAt
u.UpdatedAt = time.Now()
if err = im.userlist.Update(nameOrAlias, u); err != nil {
return err
}
user, err := im.userlist.Get(u.Name)
if err != nil {
return err
}
_, ok := im.identities[oldUser.Name]
if !ok {
return fmt.Errorf("identity not found")
}
identity, err := im.createIdentity(u)
if err != nil {
return err
}
im.identities[user.Name] = identity
im.logger.Debug().WithFields(log.Fields{
"oldname": oldUser.Name,
"newname": user.Name,
}).Log("Identity updated")
if im.autosave {
im.save()
}
return nil
}
func (im *identityManager) Delete(nameOrAlias string) error {
im.lock.Lock()
defer im.lock.Unlock()
err := im.delete(nameOrAlias)
if err != nil {
return err
}
return nil
}
func (im *identityManager) delete(nameOrAlias string) error {
if im.root.user.Name == nameOrAlias || im.root.user.Alias == nameOrAlias {
return fmt.Errorf("this identity can't be removed")
}
user, err := im.userlist.Get(nameOrAlias)
if err != nil {
return err
}
identity, ok := im.identities[user.Name]
if !ok {
return fmt.Errorf("identity not found")
}
im.userlist.Delete(user.Name)
delete(im.identities, user.Name)
identity.lock.Lock()
identity.valid = false
identity.lock.Unlock()
if len(user.Auth.API.Auth0.User) == 0 {
if im.autosave {
im.save()
}
return nil
}
delete(im.auth0UserIdentityMap, user.Auth.API.Auth0.User)
// find out if the tenant is still used somewhere else
found := false
for _, i := range im.identities {
if i.tenant == identity.tenant {
found = true
break
}
}
if !found {
identity.tenant.Cancel()
delete(im.tenants, identity.user.Auth.API.Auth0.Tenant.key())
if im.autosave {
im.save()
}
return nil
}
// find out if the tenant's clientid is still used somewhere else
found = false
for _, i := range im.identities {
if len(i.user.Auth.API.Auth0.User) == 0 {
continue
}
if i.user.Auth.API.Auth0.Tenant.ClientID == identity.user.Auth.API.Auth0.Tenant.ClientID {
found = true
break
}
}
if !found {
identity.tenant.RemoveClientID(identity.user.Auth.API.Auth0.Tenant.ClientID)
}
if im.autosave {
if err := im.save(); err != nil {
return err
}
}
return nil
}
func (im *identityManager) getIdentity(nameOrAlias string) (*identity, error) {
var identity *identity = nil
if im.root.user.Name == nameOrAlias || im.root.user.Alias == nameOrAlias {
identity = im.root
} else {
user, err := im.userlist.Get(nameOrAlias)
if err != nil {
return nil, fmt.Errorf("identity not found")
}
identity = im.identities[user.Name]
}
if identity == nil {
return nil, fmt.Errorf("identity not found")
}
identity.jwtRealm = im.jwtRealm
identity.jwtKeyFunc = func(*jwtgo.Token) (interface{}, error) { return im.jwtSecret, nil }
return identity, nil
}
func (im *identityManager) Get(nameOrAlias string) (User, error) {
im.lock.RLock()
defer im.lock.RUnlock()
identity, err := im.getIdentity(nameOrAlias)
if err != nil {
return User{}, err
}
user := identity.user.clone()
return user, nil
}
func (im *identityManager) GetVerifier(nameOrAlias string) (Verifier, error) {
im.lock.RLock()
defer im.lock.RUnlock()
return im.getIdentity(nameOrAlias)
}
func (im *identityManager) GetVerifierFromAuth0(auth0Name string) (Verifier, error) {
im.lock.RLock()
defer im.lock.RUnlock()
name, ok := im.auth0UserIdentityMap[auth0Name]
if !ok {
return nil, fmt.Errorf("not found")
}
return im.getIdentity(name)
}
func (im *identityManager) GetDefaultVerifier() (Verifier, error) {
return im.root, nil
}
func (im *identityManager) List() []User {
im.lock.RLock()
defer im.lock.RUnlock()
return im.userlist.List()
}
func (im *identityManager) Save() error {
im.lock.RLock()
defer im.lock.RUnlock()
return im.save()
}
func (im *identityManager) save() error {
users := im.userlist.List()
return im.adapter.SaveIdentities(users)
}
func (im *identityManager) Autosave(auto bool) {
im.lock.Lock()
defer im.lock.Unlock()
im.autosave = auto
}
func (im *identityManager) Validators() []string {
validators := []string{"localjwt"}
im.lock.RLock()
defer im.lock.RUnlock()
for _, t := range im.tenants {
for _, clientid := range t.clientIDs {
validators = append(validators, fmt.Sprintf("auth0 domain=%s audience=%s clientid=%s", t.domain, t.audience, clientid))
}
}
return validators
}
func (im *identityManager) CreateJWT(nameOrAlias string) (string, string, error) {
im.lock.RLock()
defer im.lock.RUnlock()
identity, err := im.getIdentity(nameOrAlias)
if err != nil {
return "", "", err
}
now := time.Now()
accessExpires := now.Add(time.Minute * 10)
refreshExpires := now.Add(time.Hour * 24)
// Create access token
accessToken := jwtgo.NewWithClaims(jwtgo.SigningMethodHS256, jwtgo.MapClaims{
"iss": im.jwtRealm,
"sub": identity.Name(),
"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(im.jwtSecret)
if err != nil {
return "", "", err
}
// Create refresh token
refreshToken := jwtgo.NewWithClaims(jwtgo.SigningMethodHS256, jwtgo.MapClaims{
"iss": im.jwtRealm,
"sub": identity.Name(),
"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(im.jwtSecret)
if err != nil {
return "", "", err
}
return at, rt, nil
}

View File

@@ -0,0 +1,824 @@
package identity
import (
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestDefaultIdentity(t *testing.T) {
adapter, err := createAdapter()
require.NoError(t, err)
im, err := New(Config{
Adapter: adapter,
Superuser: User{Name: "foobar"},
JWTRealm: "test-realm",
JWTSecret: "abc123",
Logger: nil,
})
require.NoError(t, err)
require.NotNil(t, im)
identity, err := im.GetDefaultVerifier()
require.NoError(t, err)
require.NotNil(t, identity)
require.Equal(t, "foobar", identity.Name())
}
func TestJWT(t *testing.T) {
adapter, err := createAdapter()
require.NoError(t, err)
im, err := New(Config{
Adapter: adapter,
Superuser: User{Name: "foobar"},
JWTRealm: "test-realm",
JWTSecret: "abc123",
Logger: nil,
})
require.NoError(t, err)
require.NotNil(t, im)
access, refresh, err := im.CreateJWT("foobaz")
require.Error(t, err)
require.Equal(t, "", access)
require.Equal(t, "", refresh)
access, refresh, err = im.CreateJWT("foobar")
require.NoError(t, err)
identity, err := im.GetVerifier("foobar")
require.NoError(t, err)
require.NotNil(t, identity)
ok, err := identity.VerifyJWT("something")
require.Error(t, err)
require.False(t, ok)
ok, err = identity.VerifyJWT(access)
require.NoError(t, err)
require.True(t, ok)
ok, err = identity.VerifyJWT(refresh)
require.NoError(t, err)
require.True(t, ok)
err = im.Create(User{Name: "foobaz"})
require.NoError(t, err)
access, refresh, err = im.CreateJWT("foobaz")
require.NoError(t, err)
ok, err = identity.VerifyJWT(access)
require.Error(t, err)
require.False(t, ok)
ok, err = identity.VerifyJWT(refresh)
require.Error(t, err)
require.False(t, ok)
}
func TestCreateUser(t *testing.T) {
adapter, err := createAdapter()
require.NoError(t, err)
im, err := New(Config{
Adapter: adapter,
Superuser: User{Name: "foobar", Alias: "foobalias"},
JWTRealm: "test-realm",
JWTSecret: "abc123",
Logger: nil,
})
require.NoError(t, err)
require.NotNil(t, im)
err = im.Create(User{Name: "foobar"})
require.Error(t, err)
err = im.Create(User{Name: "foobaz", Alias: "foobalias"})
require.Error(t, err)
err = im.Create(User{Name: "foobaz", Alias: "alias"})
require.NoError(t, err)
user, err := im.Get("foobaz")
require.NoError(t, err)
require.Equal(t, user.CreatedAt, user.UpdatedAt)
require.NotEqual(t, time.Time{}, user.CreatedAt)
err = im.Create(User{Name: "fooboz", Alias: "alias"})
require.Error(t, err)
err = im.Create(User{Name: "foobaz", Alias: "somealias"})
require.Error(t, err)
}
func TestAlias(t *testing.T) {
adapter, err := createAdapter()
require.NoError(t, err)
im, err := New(Config{
Adapter: adapter,
Superuser: User{Name: "foobar", Alias: "foobalias"},
JWTRealm: "test-realm",
JWTSecret: "abc123",
Logger: nil,
})
require.NoError(t, err)
require.NotNil(t, im)
err = im.Create(User{Name: "foobaz", Alias: "alias"})
require.NoError(t, err)
identity, err := im.Get("foobar")
require.NoError(t, err)
require.Equal(t, "foobar", identity.Name)
require.Equal(t, "foobalias", identity.Alias)
identity, err = im.Get("foobalias")
require.NoError(t, err)
require.Equal(t, "foobar", identity.Name)
require.Equal(t, "foobalias", identity.Alias)
identity, err = im.Get("foobaz")
require.NoError(t, err)
require.Equal(t, "foobaz", identity.Name)
require.Equal(t, "alias", identity.Alias)
identity, err = im.Get("alias")
require.NoError(t, err)
require.Equal(t, "foobaz", identity.Name)
require.Equal(t, "alias", identity.Alias)
err = im.Create(User{Name: "alias", Alias: "foobaz"})
require.Error(t, err)
}
func TestCreateUserAuth0(t *testing.T) {
adapter, err := createAdapter()
require.NoError(t, err)
im, err := New(Config{
Adapter: adapter,
Superuser: User{Name: "foobar"},
JWTRealm: "test-realm",
JWTSecret: "abc123",
Logger: nil,
})
require.NoError(t, err)
require.NotNil(t, im)
require.ElementsMatch(t, []string{"localjwt"}, im.Validators())
err = im.Create(User{
Name: "foobaz",
Superuser: false,
Auth: UserAuth{
API: UserAuthAPI{
Auth0: UserAuthAPIAuth0{
User: "auth0|123456",
Tenant: Auth0Tenant{
Domain: "example.com",
Audience: "https://api.example.com/",
ClientID: "123456",
},
},
},
},
})
require.Error(t, err)
err = im.Create(User{
Name: "foobaz",
Superuser: false,
Auth: UserAuth{
API: UserAuthAPI{
Auth0: UserAuthAPIAuth0{
User: "auth0|123456",
Tenant: Auth0Tenant{
Domain: "datarhei-demo.eu.auth0.com",
Audience: "https://datarhei-demo.eu.auth0.com/api/v2/",
ClientID: "123456",
},
},
},
},
})
require.NoError(t, err)
identity, err := im.GetVerifierFromAuth0("foobaz")
require.Error(t, err)
require.Nil(t, identity)
identity, err = im.GetVerifierFromAuth0("auth0|123456")
require.NoError(t, err)
require.NotNil(t, identity)
manager, ok := im.(*identityManager)
require.True(t, ok)
require.NotNil(t, manager)
require.Equal(t, 1, len(manager.tenants))
require.Equal(t, map[string]string{"auth0|123456": "foobaz"}, manager.auth0UserIdentityMap)
require.ElementsMatch(t, []string{
"localjwt",
"auth0 domain=datarhei-demo.eu.auth0.com audience=https://datarhei-demo.eu.auth0.com/api/v2/ clientid=123456",
}, im.Validators())
err = im.Create(User{
Name: "fooboz",
Superuser: false,
Auth: UserAuth{
API: UserAuthAPI{
Auth0: UserAuthAPIAuth0{
User: "auth0|123456",
Tenant: Auth0Tenant{
Domain: "datarhei-demo.eu.auth0.com",
Audience: "https://datarhei-demo.eu.auth0.com/api/v2/",
ClientID: "123456",
},
},
},
},
})
require.Error(t, err)
err = im.Create(User{
Name: "fooboz",
Superuser: false,
Auth: UserAuth{
API: UserAuthAPI{
Auth0: UserAuthAPIAuth0{
User: "auth0|987654",
Tenant: Auth0Tenant{
Domain: "datarhei-demo.eu.auth0.com",
Audience: "https://datarhei-demo.eu.auth0.com/api/v2/",
ClientID: "987654",
},
},
},
},
})
require.NoError(t, err)
require.Equal(t, 1, len(manager.tenants))
require.Equal(t, map[string]string{"auth0|123456": "foobaz", "auth0|987654": "fooboz"}, manager.auth0UserIdentityMap)
require.ElementsMatch(t, []string{
"localjwt",
"auth0 domain=datarhei-demo.eu.auth0.com audience=https://datarhei-demo.eu.auth0.com/api/v2/ clientid=123456",
"auth0 domain=datarhei-demo.eu.auth0.com audience=https://datarhei-demo.eu.auth0.com/api/v2/ clientid=987654",
}, im.Validators())
im.Close()
}
func TestLoadAndSave(t *testing.T) {
adptr, err := createAdapter()
require.NoError(t, err)
dummyfs := adptr.(*fileAdapter).fs
im, err := New(Config{
Adapter: adptr,
Superuser: User{Name: "foobar"},
JWTRealm: "test-realm",
JWTSecret: "abc123",
Logger: nil,
})
require.NoError(t, err)
require.NotNil(t, im)
err = im.Save()
require.NoError(t, err)
_, err = dummyfs.Stat("./users.json")
require.NoError(t, err)
data, err := dummyfs.ReadFile("./users.json")
require.NoError(t, err)
require.Equal(t, []byte("[]"), data)
err = im.Create(User{Name: "foobaz", Alias: "alias"})
require.NoError(t, err)
identity, err := im.GetVerifier("foobaz")
require.NoError(t, err)
require.NotNil(t, identity)
identity, err = im.GetVerifier("alias")
require.NoError(t, err)
require.NotNil(t, identity)
user, err := im.Get("foobaz")
require.NoError(t, err)
createdAt := user.CreatedAt
updatedAt := user.UpdatedAt
err = im.Save()
require.NoError(t, err)
im, err = New(Config{
Adapter: adptr,
Superuser: User{Name: "foobar"},
JWTRealm: "test-realm",
JWTSecret: "abc123",
Logger: nil,
})
require.NoError(t, err)
require.NotNil(t, im)
identity, err = im.GetVerifier("foobaz")
require.NoError(t, err)
require.NotNil(t, identity)
identity, err = im.GetVerifier("alias")
require.NoError(t, err)
require.NotNil(t, identity)
user, err = im.Get("foobaz")
require.NoError(t, err)
require.True(t, createdAt.Equal(user.CreatedAt))
require.True(t, updatedAt.Equal(user.UpdatedAt))
}
func TestReloadIdempotent(t *testing.T) {
adapter, err := createAdapter()
require.NoError(t, err)
im, err := New(Config{
Adapter: adapter,
Superuser: User{Name: "foobar"},
JWTRealm: "test-realm",
JWTSecret: "abc123",
Logger: nil,
})
require.NoError(t, err)
require.NotNil(t, im)
err = im.Create(User{Name: "foobaz", Alias: "alias"})
require.NoError(t, err)
err = im.Save()
require.NoError(t, err)
_, err = im.Get("foobaz")
require.NoError(t, err)
err = im.Reload()
require.NoError(t, err)
_, err = im.Get("foobaz")
require.NoError(t, err)
err = im.Reload()
require.NoError(t, err)
_, err = im.Get("foobaz")
require.NoError(t, err)
}
func TestUpdateUser(t *testing.T) {
adapter, err := createAdapter()
require.NoError(t, err)
im, err := New(Config{
Adapter: adapter,
Superuser: User{Name: "foobar"},
JWTRealm: "test-realm",
JWTSecret: "abc123",
Logger: nil,
})
require.NoError(t, err)
require.NotNil(t, im)
err = im.Create(User{Name: "fooboz"})
require.NoError(t, err)
user, err := im.Get("fooboz")
require.NoError(t, err)
require.True(t, user.CreatedAt.Equal(user.UpdatedAt))
require.False(t, time.Time{}.Equal(user.CreatedAt))
err = im.Update("unknown", User{Name: "fooboz"})
require.Error(t, err)
err = im.Update("foobar", User{Name: "foobar"})
require.Error(t, err)
err = im.Update("foobar", User{Name: "fooboz"})
require.Error(t, err)
identity, err := im.GetVerifier("foobar")
require.NoError(t, err)
require.NotNil(t, identity)
require.Equal(t, "foobar", identity.Name())
err = im.Update("foobar", User{Name: "foobaz"})
require.Error(t, err)
require.Equal(t, "foobar", identity.Name())
identity, err = im.GetVerifier("foobaz")
require.Error(t, err)
require.Nil(t, identity)
identity, err = im.GetVerifier("fooboz")
require.NoError(t, err)
require.NotNil(t, identity)
require.Equal(t, "fooboz", identity.Name())
time.Sleep(1 * time.Second)
err = im.Update("fooboz", User{Name: "foobaz"})
require.NoError(t, err)
user, err = im.Get("foobaz")
require.NoError(t, err)
require.True(t, user.CreatedAt.Equal(user.CreatedAt))
require.False(t, user.CreatedAt.Equal(user.UpdatedAt))
require.False(t, time.Time{}.Equal(user.CreatedAt))
}
func TestUpdateUserAlias(t *testing.T) {
adapter, err := createAdapter()
require.NoError(t, err)
im, err := New(Config{
Adapter: adapter,
Superuser: User{Name: "foobar", Alias: "superalias"},
JWTRealm: "test-realm",
JWTSecret: "abc123",
Logger: nil,
})
require.NoError(t, err)
require.NotNil(t, im)
err = im.Create(User{Name: "fooboz"})
require.NoError(t, err)
identity, err := im.GetVerifier("fooboz")
require.NoError(t, err)
require.NotNil(t, identity)
require.Equal(t, "fooboz", identity.Name())
_, err = im.GetVerifier("alias")
require.Error(t, err)
err = im.Update("fooboz", User{Name: "fooboz", Alias: "alias"})
require.NoError(t, err)
identity, err = im.GetVerifier("fooboz")
require.NoError(t, err)
require.NotNil(t, identity)
require.Equal(t, "fooboz", identity.Name())
require.Equal(t, "alias", identity.Alias())
err = im.Create(User{Name: "barfoo", Alias: "alias2"})
require.NoError(t, err)
err = im.Update("fooboz", User{Name: "fooboz", Alias: "alias2"})
require.Error(t, err)
err = im.Update("fooboz", User{Name: "barfoo", Alias: "alias"})
require.Error(t, err)
err = im.Update("fooboz", User{Name: "fooboz", Alias: "superalias"})
require.Error(t, err)
err = im.Update("fooboz", User{Name: "foobar", Alias: ""})
require.Error(t, err)
}
func TestUpdateUserAuth0(t *testing.T) {
adapter, err := createAdapter()
require.NoError(t, err)
im, err := New(Config{
Adapter: adapter,
Superuser: User{Name: "foobar"},
JWTRealm: "test-realm",
JWTSecret: "abc123",
Logger: nil,
})
require.NoError(t, err)
require.NotNil(t, im)
err = im.Create(User{
Name: "foobaz",
Superuser: false,
Auth: UserAuth{
API: UserAuthAPI{
Auth0: UserAuthAPIAuth0{
User: "auth0|123456",
Tenant: Auth0Tenant{
Domain: "datarhei-demo.eu.auth0.com",
Audience: "https://datarhei-demo.eu.auth0.com/api/v2/",
ClientID: "123456",
},
},
},
},
})
require.NoError(t, err)
identity, err := im.GetVerifierFromAuth0("auth0|123456")
require.NoError(t, err)
require.NotNil(t, identity)
identity, err = im.GetVerifier("foobaz")
require.NoError(t, err)
require.NotNil(t, identity)
user, err := im.Get("foobaz")
require.NoError(t, err)
user.Name = "fooboz"
err = im.Update("foobaz", user)
require.NoError(t, err)
identity, err = im.GetVerifierFromAuth0("auth0|123456")
require.NoError(t, err)
require.NotNil(t, identity)
identity, err = im.GetVerifier("fooboz")
require.NoError(t, err)
require.NotNil(t, identity)
}
func TestRemoveUser(t *testing.T) {
adapter, err := createAdapter()
require.NoError(t, err)
im, err := New(Config{
Adapter: adapter,
Superuser: User{Name: "foobar"},
JWTRealm: "test-realm",
JWTSecret: "abc123",
Logger: nil,
})
require.NoError(t, err)
require.NotNil(t, im)
err = im.Delete("fooboz")
require.Error(t, err)
err = im.Delete("foobar")
require.Error(t, err)
err = im.Create(User{
Name: "foobaz",
Superuser: false,
Auth: UserAuth{
API: UserAuthAPI{
Password: "apisecret",
Auth0: UserAuthAPIAuth0{},
},
Services: UserAuthServices{
Basic: []string{"secret"},
Token: []string{"tokensecret"},
Session: []string{"sessionsecret"},
},
},
})
require.NoError(t, err)
identity, err := im.GetVerifier("foobaz")
require.NoError(t, err)
require.NotNil(t, identity)
ok, err := identity.VerifyAPIPassword("apisecret")
require.True(t, ok)
require.NoError(t, err)
ok, err = identity.VerifyServiceBasicAuth("secret")
require.True(t, ok)
require.NoError(t, err)
ok, err = identity.VerifyServiceToken("tokensecret")
require.True(t, ok)
require.NoError(t, err)
session := identity.GetServiceSession(nil, time.Hour)
ok, data, err := identity.VerifyServiceSession(session)
require.True(t, ok)
require.Equal(t, nil, data)
require.NoError(t, err)
access, refresh, err := im.CreateJWT("foobaz")
require.NoError(t, err)
ok, err = identity.VerifyJWT(access)
require.True(t, ok)
require.NoError(t, err)
ok, err = identity.VerifyJWT(refresh)
require.True(t, ok)
require.NoError(t, err)
err = im.Delete("foobaz")
require.NoError(t, err)
ok, err = identity.VerifyAPIPassword("apisecret")
require.False(t, ok)
require.Error(t, err)
ok, err = identity.VerifyServiceBasicAuth("secret")
require.False(t, ok)
require.Error(t, err)
ok, err = identity.VerifyServiceToken("tokensecret")
require.False(t, ok)
require.Error(t, err)
ok, data, err = identity.VerifyServiceSession(session)
require.False(t, ok)
require.Equal(t, nil, data)
require.Error(t, err)
ok, err = identity.VerifyJWT(access)
require.False(t, ok)
require.Error(t, err)
ok, err = identity.VerifyJWT(refresh)
require.False(t, ok)
require.Error(t, err)
identity, err = im.GetVerifier("foobaz")
require.Error(t, err)
require.Nil(t, identity)
access, refresh, err = im.CreateJWT("foobaz")
require.Error(t, err)
require.Empty(t, access)
require.Empty(t, refresh)
}
func TestRemoveUserAuth0(t *testing.T) {
adapter, err := createAdapter()
require.NoError(t, err)
im, err := New(Config{
Adapter: adapter,
Superuser: User{Name: "foobar"},
JWTRealm: "test-realm",
JWTSecret: "abc123",
Logger: nil,
})
require.NoError(t, err)
require.NotNil(t, im)
err = im.Create(User{
Name: "foobaz",
Superuser: false,
Auth: UserAuth{
API: UserAuthAPI{
Auth0: UserAuthAPIAuth0{
User: "auth0|123456",
Tenant: Auth0Tenant{
Domain: "datarhei-demo.eu.auth0.com",
Audience: "https://datarhei-demo.eu.auth0.com/api/v2/",
ClientID: "123456",
},
},
},
},
})
require.NoError(t, err)
err = im.Create(User{
Name: "fooboz",
Superuser: false,
Auth: UserAuth{
API: UserAuthAPI{
Auth0: UserAuthAPIAuth0{
User: "auth0|987654",
Tenant: Auth0Tenant{
Domain: "datarhei-demo.eu.auth0.com",
Audience: "https://datarhei-demo.eu.auth0.com/api/v2/",
ClientID: "987654",
},
},
},
},
})
require.NoError(t, err)
manager, ok := im.(*identityManager)
require.True(t, ok)
require.NotNil(t, manager)
require.Equal(t, 1, len(manager.tenants))
require.Equal(t, map[string]string{"auth0|123456": "foobaz", "auth0|987654": "fooboz"}, manager.auth0UserIdentityMap)
require.ElementsMatch(t, []string{
"localjwt",
"auth0 domain=datarhei-demo.eu.auth0.com audience=https://datarhei-demo.eu.auth0.com/api/v2/ clientid=123456",
"auth0 domain=datarhei-demo.eu.auth0.com audience=https://datarhei-demo.eu.auth0.com/api/v2/ clientid=987654",
}, im.Validators())
err = im.Delete("foobaz")
require.NoError(t, err)
require.Equal(t, 1, len(manager.tenants))
require.Equal(t, map[string]string{"auth0|987654": "fooboz"}, manager.auth0UserIdentityMap)
require.ElementsMatch(t, []string{
"localjwt",
"auth0 domain=datarhei-demo.eu.auth0.com audience=https://datarhei-demo.eu.auth0.com/api/v2/ clientid=987654",
}, im.Validators())
err = im.Delete("fooboz")
require.NoError(t, err)
require.Equal(t, 0, len(manager.tenants))
require.ElementsMatch(t, []string{
"localjwt",
}, im.Validators())
}
func TestAutosave(t *testing.T) {
adptr, err := createAdapter()
require.NoError(t, err)
dummyfs := adptr.(*fileAdapter).fs
im, err := New(Config{
Adapter: adptr,
Superuser: User{Name: "foobar"},
JWTRealm: "test-realm",
JWTSecret: "abc123",
Logger: nil,
})
require.NoError(t, err)
require.NotNil(t, im)
err = im.Save()
require.NoError(t, err)
_, err = dummyfs.Stat("./users.json")
require.NoError(t, err)
data, err := dummyfs.ReadFile("./users.json")
require.NoError(t, err)
require.Equal(t, []byte("[]"), data)
err = im.Create(User{Name: "foobaz"})
require.NoError(t, err)
data, err = dummyfs.ReadFile("./users.json")
require.NoError(t, err)
require.NotEqual(t, []byte("[]"), data)
user, err := im.Get("foobaz")
require.NoError(t, err)
user.Name = "fooboz"
err = im.Update("foobaz", user)
require.NoError(t, err)
data, err = dummyfs.ReadFile("./users.json")
require.NoError(t, err)
require.NotEqual(t, []byte("[]"), data)
err = im.Delete("fooboz")
require.NoError(t, err)
data, err = dummyfs.ReadFile("./users.json")
require.NoError(t, err)
require.Equal(t, []byte("[]"), data)
}
func TestList(t *testing.T) {
adapter, err := createAdapter()
require.NoError(t, err)
im, err := New(Config{
Adapter: adapter,
Superuser: User{Name: "foobar"},
JWTRealm: "test-realm",
JWTSecret: "abc123",
Logger: nil,
})
require.NoError(t, err)
require.NotNil(t, im)
err = im.Create(User{Name: "fooboz"})
require.NoError(t, err)
users := im.List()
require.Equal(t, 1, len(users))
err = im.Create(User{Name: "foobaz", Alias: "alias"})
require.NoError(t, err)
users = im.List()
require.Equal(t, 2, len(users))
}

261
iam/identity/user.go Normal file
View File

@@ -0,0 +1,261 @@
package identity
import (
"fmt"
"strings"
"time"
"github.com/datarhei/core/v16/slices"
)
type User struct {
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Name string `json:"name"`
Alias string `json:"alias"`
Superuser bool `json:"superuser"`
Auth UserAuth `json:"auth"`
}
type UserAuth struct {
API UserAuthAPI `json:"api"`
Services UserAuthServices `json:"services"`
}
type UserAuthAPI struct {
Password string `json:"password"`
Auth0 UserAuthAPIAuth0 `json:"auth0"`
}
type UserAuthAPIAuth0 struct {
User string `json:"user"`
Tenant Auth0Tenant `json:"tenant"`
}
type UserAuthServices struct {
Basic []string `json:"basic"` // Passwords for BasicAuth
Token []string `json:"token"` // Tokens/Streamkey for RTMP and SRT
Session []string `json:"session"` // Secrets for session JWT
}
func (u *User) Validate() error {
if len(u.Name) == 0 {
return fmt.Errorf("a name is required")
}
if strings.HasPrefix(u.Name, "$") {
return fmt.Errorf("name is not allowed to start with $")
}
if len(u.Alias) != 0 {
if strings.HasPrefix(u.Alias, "$") {
return fmt.Errorf("alias is not allowed to start with $")
}
}
if len(u.Auth.API.Auth0.User) != 0 {
t, err := newAuth0Tenant(u.Auth.API.Auth0.Tenant)
if err != nil {
return fmt.Errorf("auth0: %w", err)
}
t.Cancel()
}
return nil
}
func (u *User) marshalIdentity() *identity {
i := &identity{
user: u.clone(),
}
return i
}
func (u *User) clone() User {
user := *u
user.Auth.Services.Basic = slices.Copy(u.Auth.Services.Basic)
user.Auth.Services.Token = slices.Copy(u.Auth.Services.Token)
user.Auth.Services.Session = slices.Copy(u.Auth.Services.Session)
return user
}
type UserList interface {
Add(u User) error
Get(nameOrAlias string) (User, error)
Update(nameOrAlias string, u User) error
Delete(nameorAlias string)
List() []User
}
type userlist struct {
namesUserMap map[string]string
auth0UserMap map[string]string
user map[string]User
}
func NewUserList() UserList {
return &userlist{
namesUserMap: map[string]string{},
auth0UserMap: map[string]string{},
user: map[string]User{},
}
}
// Add implements UserList.
func (ul *userlist) Add(u User) error {
if err := u.Validate(); err != nil {
return fmt.Errorf("invalid user: %w", err)
}
if _, ok := ul.namesUserMap[u.Name]; ok {
return fmt.Errorf("the name '%s' is already in use", u.Name)
}
if len(u.Alias) != 0 {
if _, ok := ul.namesUserMap[u.Alias]; ok {
return fmt.Errorf("the alias '%s' is already in use", u.Alias)
}
}
if len(u.Auth.API.Auth0.User) != 0 {
if name, ok := ul.auth0UserMap[u.Auth.API.Auth0.User]; ok {
return fmt.Errorf("the Auth0 user has already an identity (%s)", name)
}
}
u = u.clone()
ul.namesUserMap[u.Name] = u.Name
if len(u.Alias) != 0 {
ul.namesUserMap[u.Alias] = u.Name
}
if len(u.Auth.API.Auth0.User) != 0 {
ul.auth0UserMap[u.Auth.API.Auth0.User] = u.Name
}
ul.user[u.Name] = u
return nil
}
func (ul *userlist) Get(nameOrAlias string) (User, error) {
name, ok := ul.namesUserMap[nameOrAlias]
if !ok {
return User{}, fmt.Errorf("user not found")
}
u, ok := ul.user[name]
if !ok {
return User{}, fmt.Errorf("user not found")
}
return u.clone(), nil
}
// Delete implements UserList.
func (ul *userlist) Delete(nameOrAlias string) {
name, ok := ul.namesUserMap[nameOrAlias]
if !ok {
return
}
u, ok := ul.user[name]
if !ok {
delete(ul.namesUserMap, nameOrAlias)
return
}
delete(ul.namesUserMap, u.Name)
delete(ul.namesUserMap, u.Alias)
delete(ul.auth0UserMap, u.Auth.API.Auth0.User)
delete(ul.user, u.Name)
}
// List implements UserList.
func (ul *userlist) List() []User {
user := []User{}
for _, u := range ul.user {
user = append(user, u.clone())
}
return user
}
// Update implements UserList.
func (ul *userlist) Update(nameOrAlias string, u User) error {
if err := u.Validate(); err != nil {
return fmt.Errorf("invalid user: %w", err)
}
name, ok := ul.namesUserMap[nameOrAlias]
if !ok {
return fmt.Errorf("user with the name or alias '%s' not found", nameOrAlias)
}
oldUser, ok := ul.user[name]
if !ok {
return fmt.Errorf("user with the name '%s' not found", name)
}
delete(ul.namesUserMap, oldUser.Name)
delete(ul.namesUserMap, oldUser.Alias)
delete(ul.auth0UserMap, oldUser.Auth.API.Auth0.User)
if _, ok := ul.namesUserMap[u.Name]; ok {
ul.namesUserMap[oldUser.Name] = oldUser.Name
if len(oldUser.Alias) != 0 {
ul.namesUserMap[oldUser.Alias] = oldUser.Name
}
if len(oldUser.Auth.API.Auth0.User) != 0 {
ul.auth0UserMap[oldUser.Auth.API.Auth0.User] = oldUser.Name
}
return fmt.Errorf("the name '%s' is already in use", u.Name)
}
if len(u.Alias) != 0 {
if _, ok := ul.namesUserMap[u.Alias]; ok {
ul.namesUserMap[oldUser.Name] = oldUser.Name
if len(oldUser.Alias) != 0 {
ul.namesUserMap[oldUser.Alias] = oldUser.Name
}
if len(oldUser.Auth.API.Auth0.User) != 0 {
ul.auth0UserMap[oldUser.Auth.API.Auth0.User] = oldUser.Name
}
return fmt.Errorf("the alias '%s' is already in use", u.Alias)
}
}
if len(u.Auth.API.Auth0.User) != 0 {
if _, ok := ul.auth0UserMap[u.Auth.API.Auth0.User]; ok {
ul.namesUserMap[oldUser.Name] = oldUser.Name
if len(oldUser.Alias) != 0 {
ul.namesUserMap[oldUser.Alias] = oldUser.Name
}
if len(oldUser.Auth.API.Auth0.User) != 0 {
ul.auth0UserMap[oldUser.Auth.API.Auth0.User] = oldUser.Name
}
return fmt.Errorf("the Auth0 user has already an identity (%s)", u.Auth.API.Auth0.User)
}
}
delete(ul.user, oldUser.Name)
u = u.clone()
ul.namesUserMap[u.Name] = u.Name
if len(u.Alias) != 0 {
ul.namesUserMap[u.Alias] = u.Name
}
if len(u.Auth.API.Auth0.User) != 0 {
ul.auth0UserMap[u.Auth.API.Auth0.User] = u.Name
}
ul.user[u.Name] = u
return nil
}

146
iam/identity/user_test.go Normal file
View File

@@ -0,0 +1,146 @@
package identity
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestUserName(t *testing.T) {
user := User{}
err := user.Validate()
require.Error(t, err)
user.Name = "foobar_5"
err = user.Validate()
require.NoError(t, err)
user.Name = "foobar:5"
err = user.Validate()
require.NoError(t, err)
user.Name = "$foob:ar"
err = user.Validate()
require.Error(t, err)
}
func TestUserAlias(t *testing.T) {
user := User{
Name: "foober",
}
err := user.Validate()
require.NoError(t, err)
user.Alias = "foobar"
err = user.Validate()
require.NoError(t, err)
user.Alias = "$foob:ar"
err = user.Validate()
require.Error(t, err)
}
func TestUserListAdd(t *testing.T) {
l := NewUserList()
_, err := l.Get("foobar")
require.Error(t, err)
err = l.Add(User{Name: "foobar"})
require.NoError(t, err)
_, err = l.Get("foobar")
require.NoError(t, err)
err = l.Add(User{Name: "foobaz", Alias: "foobar"})
require.Error(t, err)
err = l.Add(User{Name: "foobaz", Alias: "foobaz"})
require.NoError(t, err)
err = l.Add(User{Name: "barfoo", Alias: "foobaz"})
require.Error(t, err)
}
func TestUserListDelete(t *testing.T) {
l := NewUserList()
_, err := l.Get("foobar")
require.Error(t, err)
err = l.Add(User{Name: "foobar"})
require.NoError(t, err)
_, err = l.Get("foobar")
require.NoError(t, err)
l.Delete("foobar")
require.NoError(t, err)
_, err = l.Get("foobar")
require.Error(t, err)
err = l.Add(User{Name: "foobar", Alias: "foobaz"})
require.NoError(t, err)
_, err = l.Get("foobaz")
require.NoError(t, err)
l.Delete("foobaz")
require.NoError(t, err)
_, err = l.Get("foobaz")
require.Error(t, err)
}
func TestUserListUpdate(t *testing.T) {
l := NewUserList()
err := l.Add(User{Name: "foobar"})
require.NoError(t, err)
err = l.Update("foobaz", User{Name: "foobar"})
require.Error(t, err)
err = l.Update("foobar", User{Name: "foobaz", Alias: "fooboz"})
require.NoError(t, err)
_, err = l.Get("foobar")
require.Error(t, err)
_, err = l.Get("foobaz")
require.NoError(t, err)
_, err = l.Get("fooboz")
require.NoError(t, err)
err = l.Add(User{Name: "foobar"})
require.NoError(t, err)
err = l.Update("foobaz", User{Name: "foobar"})
require.Error(t, err)
err = l.Update("fooboz", User{Name: "fooboz"})
require.NoError(t, err)
_, err = l.Get("foobaz")
require.Error(t, err)
}
func TestUserListList(t *testing.T) {
l := NewUserList()
err := l.Add(User{Name: "foobar", Alias: "foobaz"})
require.NoError(t, err)
users := l.List()
require.Equal(t, 1, len(users))
err = l.Add(User{Name: "barfoo", Alias: "bazfoo"})
require.NoError(t, err)
users = l.List()
require.Equal(t, 2, len(users))
}