mirror of
https://github.com/datarhei/core.git
synced 2025-10-07 17:01:01 +08:00
Add CreatedAt and UpdatedAt field to IAM user
This commit is contained in:
@@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -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
127
cluster/store/identity.go
Normal 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
|
||||||
|
}
|
445
cluster/store/identity_test.go
Normal file
445
cluster/store/identity_test.go
Normal 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
63
cluster/store/kvs.go
Normal 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
113
cluster/store/kvs_test.go
Normal 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
74
cluster/store/lock.go
Normal 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
167
cluster/store/lock_test.go
Normal 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
77
cluster/store/policy.go
Normal 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
|
||||||
|
}
|
108
cluster/store/policy_test.go
Normal file
108
cluster/store/policy_test.go
Normal 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
228
cluster/store/process.go
Normal 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
|
||||||
|
}
|
647
cluster/store/process_test.go
Normal file
647
cluster/store/process_test.go
Normal 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)
|
||||||
|
}
|
@@ -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
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -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:
|
||||||
|
@@ -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,
|
||||||
|
14
iam/iam.go
14
iam/iam.go
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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
|
|
||||||
}
|
|
||||||
|
@@ -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
536
iam/identity/manager.go
Normal 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
|
||||||
|
}
|
824
iam/identity/manager_test.go
Normal file
824
iam/identity/manager_test.go
Normal 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
261
iam/identity/user.go
Normal 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
146
iam/identity/user_test.go
Normal 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))
|
||||||
|
}
|
Reference in New Issue
Block a user