diff --git a/iam/identity.go b/iam/identity.go index 2fa9feef..5d83b3e5 100644 --- a/iam/identity.go +++ b/iam/identity.go @@ -86,6 +86,15 @@ func (u *User) marshalIdentity() *identity { return i } +func (u *User) clone() User { + user := *u + + user.Auth.Services.Token = make([]string, len(u.Auth.Services.Token)) + copy(user.Auth.Services.Token, u.Auth.Services.Token) + + return user +} + type IdentityVerifier interface { Name() string @@ -238,12 +247,30 @@ func (i *identity) auth0KeyFunc(token *jwtgo.Token) (interface{}, error) { } func (i *identity) VerifyJWT(jwt string) (bool, error) { + i.lock.RLock() + defer i.lock.RUnlock() + + if !i.isValid() { + return false, fmt.Errorf("invalid identity") + } + p := &jwtgo.Parser{} token, _, err := p.ParseUnverified(jwt, jwtgo.MapClaims{}) if err != nil { return false, err } + var subject string + if claims, ok := token.Claims.(jwtgo.MapClaims); ok { + if sub, ok := claims["sub"]; ok { + subject = sub.(string) + } + } + + if subject != i.user.Name { + return false, fmt.Errorf("wrong subject") + } + var issuer string if claims, ok := token.Claims.(jwtgo.MapClaims); ok { if sub, ok := claims["iss"]; ok { @@ -346,17 +373,19 @@ func (i *identity) IsSuperuser() bool { type IdentityManager interface { Create(identity User) error + Update(name string, identity User) error Remove(name string) error + + Get(name string) (User, error) GetVerifier(name string) (IdentityVerifier, error) GetVerifierByAuth0(name string) (IdentityVerifier, error) GetDefaultVerifier() (IdentityVerifier, error) - Rename(oldname, newname string) error - Update(name string, identity User) error Validators() []string CreateJWT(name string) (string, string, error) Save() error + Autosave(bool) Close() } @@ -370,6 +399,7 @@ type identityManager struct { fs fs.Filesystem filePath string + autosave bool logger log.Logger jwtRealm string @@ -446,6 +476,10 @@ func (im *identityManager) Create(u User) error { im.lock.Lock() defer im.lock.Unlock() + if im.root != nil && im.root.user.Name == u.Name { + return fmt.Errorf("identity already exists") + } + _, ok := im.identities[u.Name] if ok { return fmt.Errorf("identity already exists") @@ -458,10 +492,15 @@ func (im *identityManager) Create(u User) error { im.identities[identity.user.Name] = identity + if im.autosave { + im.save(im.filePath) + } + return nil } func (im *identityManager) create(u User) (*identity, error) { + u = u.clone() identity := u.marshalIdentity() if identity.user.Auth.API.Auth0.Enable { @@ -471,7 +510,7 @@ func (im *identityManager) create(u User) (*identity, error) { auth0Key := identity.user.Auth.API.Auth0.Tenant.key() - if _, ok := im.tenants[auth0Key]; !ok { + if tenant, ok := im.tenants[auth0Key]; !ok { tenant, err := newAuth0Tenant(identity.user.Auth.API.Auth0.Tenant) if err != nil { return nil, err @@ -479,7 +518,12 @@ func (im *identityManager) create(u User) (*identity, error) { 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 @@ -487,7 +531,52 @@ func (im *identityManager) create(u User) (*identity, error) { return identity, nil } -func (im *identityManager) Update(name string, identity User) error { +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 { + return fmt.Errorf("this identity can't be updated") + } + + oldidentity, ok := im.identities[name] + if !ok { + return fmt.Errorf("not found") + } + + if name != u.Name { + _, err := im.getIdentity(u.Name) + if err == nil { + return fmt.Errorf("identity already exist") + } + } + + err := im.remove(name) + if err != nil { + return err + } + + identity, err := im.create(u) + if err != nil { + if identity, err := im.create(oldidentity.user); err != nil { + return err + } else { + im.identities[identity.user.Name] = identity + } + + return err + } + + im.identities[identity.user.Name] = identity + + if im.autosave { + im.save(im.filePath) + } + return nil } @@ -495,16 +584,75 @@ func (im *identityManager) Remove(name string) error { im.lock.Lock() defer im.lock.Unlock() - user, ok := im.identities[name] + return im.remove(name) +} + +func (im *identityManager) remove(name string) error { + if im.root.user.Name == name { + return fmt.Errorf("this identity can't be removed") + } + + identity, ok := im.identities[name] if !ok { - return nil + return fmt.Errorf("not found") } delete(im.identities, name) - user.lock.Lock() - user.valid = false - user.lock.Unlock() + identity.lock.Lock() + identity.valid = false + identity.lock.Unlock() + + if !identity.user.Auth.API.Auth0.Enable { + if im.autosave { + im.save(im.filePath) + } + + 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(im.filePath) + } + + return nil + } + + // find out if the tenant's clientid is still used somewhere else + found = false + for _, i := range im.identities { + if !i.user.Auth.API.Auth0.Enable { + 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 { + im.save(im.filePath) + } return nil } @@ -516,7 +664,6 @@ func (im *identityManager) getIdentity(name string) (*identity, error) { identity = im.root } else { identity = im.identities[name] - } if identity == nil { @@ -529,6 +676,20 @@ func (im *identityManager) getIdentity(name string) (*identity, error) { 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) (IdentityVerifier, error) { im.lock.RLock() defer im.lock.RUnlock() @@ -552,27 +713,6 @@ func (im *identityManager) GetDefaultVerifier() (IdentityVerifier, error) { return im.root, nil } -func (im *identityManager) Rename(oldname, newname string) error { - im.lock.Lock() - defer im.lock.Unlock() - - identity, ok := im.identities[oldname] - if !ok { - return nil - } - - if _, ok := im.identities[newname]; ok { - return fmt.Errorf("the new name already exists") - } - - delete(im.identities, oldname) - - identity.user.Name = newname - im.identities[newname] = identity - - return nil -} - func (im *identityManager) load(filePath string) error { if _, err := im.fs.Stat(filePath); os.IsNotExist(err) { return nil @@ -601,6 +741,9 @@ func (im *identityManager) load(filePath string) error { } func (im *identityManager) Save() error { + im.lock.RLock() + defer im.lock.RUnlock() + return im.save(im.filePath) } @@ -609,9 +752,6 @@ func (im *identityManager) save(filePath string) error { return fmt.Errorf("invalid file path, file path cannot be empty") } - im.lock.RLock() - defer im.lock.RUnlock() - users := []User{} for _, u := range im.identities { @@ -628,6 +768,13 @@ func (im *identityManager) save(filePath string) error { return err } +func (im *identityManager) Autosave(auto bool) { + im.lock.Lock() + defer im.lock.Unlock() + + im.autosave = auto +} + func (im *identityManager) Validators() []string { validators := []string{"localjwt"} @@ -644,6 +791,14 @@ func (im *identityManager) Validators() []string { } 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) @@ -651,7 +806,7 @@ func (im *identityManager) CreateJWT(name string) (string, string, error) { // Create access token accessToken := jwtgo.NewWithClaims(jwtgo.SigningMethodHS256, jwtgo.MapClaims{ "iss": im.jwtRealm, - "sub": name, + "sub": identity.Name(), "usefor": "access", "iat": now.Unix(), "exp": accessExpires.Unix(), @@ -668,7 +823,7 @@ func (im *identityManager) CreateJWT(name string) (string, string, error) { // Create refresh token refreshToken := jwtgo.NewWithClaims(jwtgo.SigningMethodHS256, jwtgo.MapClaims{ "iss": im.jwtRealm, - "sub": name, + "sub": identity.Name(), "usefor": "refresh", "iat": now.Unix(), "exp": refreshExpires.Unix(), @@ -701,6 +856,8 @@ type auth0Tenant struct { audience string clientIDs []string certs jwks.JWKS + + lock sync.Mutex } func newAuth0Tenant(tenant Auth0Tenant) (*auth0Tenant, error) { @@ -712,7 +869,7 @@ func newAuth0Tenant(tenant Auth0Tenant) (*auth0Tenant, error) { certs: nil, } - url := t.issuer + "/.well-known/jwks.json" + url := t.issuer + ".well-known/jwks.json" certs, err := jwks.NewFromURL(url, jwks.Config{}) if err != nil { return nil, err @@ -726,3 +883,39 @@ func newAuth0Tenant(tenant Auth0Tenant) (*auth0Tenant, error) { func (a *auth0Tenant) Cancel() { a.certs.Cancel() } + +func (a *auth0Tenant) AddClientID(clientid string) { + a.lock.Lock() + defer a.lock.Unlock() + + found := false + for _, id := range a.clientIDs { + if id == clientid { + found = true + break + } + } + + if found { + return + } + + a.clientIDs = append(a.clientIDs, clientid) +} + +func (a *auth0Tenant) RemoveClientID(clientid string) { + a.lock.Lock() + defer a.lock.Unlock() + + clientids := []string{} + + for _, id := range a.clientIDs { + if id == clientid { + continue + } + + clientids = append(clientids, id) + } + + a.clientIDs = clientids +} diff --git a/iam/identity_test.go b/iam/identity_test.go index 7c04b17b..f6a07fe4 100644 --- a/iam/identity_test.go +++ b/iam/identity_test.go @@ -1 +1,802 @@ package iam + +import ( + "testing" + + "github.com/datarhei/core/v16/io/fs" + "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 = "$foob:ar" + err = user.validate() + require.Error(t, err) +} + +func TestUserAuth(t *testing.T) { + user := User{ + Name: "foobar", + } + + err := user.validate() + require.NoError(t, err) + + user.Auth.API.Userpass.Enable = true + err = user.validate() + require.Error(t, err) + + user.Auth.API.Userpass.Password = "secret" + err = user.validate() + require.NoError(t, err) + + user.Auth.API.Auth0.Enable = true + err = user.validate() + require.Error(t, err) + + user.Auth.API.Auth0.User = "auth0|123456" + err = user.validate() + require.NoError(t, err) + + user.Auth.Services.Basic.Enable = true + err = user.validate() + require.Error(t, err) + + user.Auth.Services.Basic.Password = "secret" + err = user.validate() + require.NoError(t, err) +} + +func TestIdentity(t *testing.T) { + user := User{ + Name: "foobar", + } + + identity := user.marshalIdentity() + + require.Equal(t, "foobar", identity.Name()) + + require.False(t, identity.isValid()) + identity.valid = true + require.True(t, identity.isValid()) + + require.False(t, identity.IsSuperuser()) + identity.user.Superuser = true + require.True(t, identity.IsSuperuser()) +} + +func TestDefaultIdentity(t *testing.T) { + dummyfs, err := fs.NewMemFilesystem(fs.MemConfig{}) + require.NoError(t, err) + + im, err := NewIdentityManager(IdentityConfig{ + FS: dummyfs, + 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) { + user := User{ + Name: "foobar", + } + + identity := user.marshalIdentity() + + ok, err := identity.VerifyAPIPassword("secret") + require.False(t, ok) + require.Error(t, err) + + identity.user.Auth.API.Userpass.Enable = true + identity.user.Auth.API.Userpass.Password = "secret" + + ok, err = identity.VerifyAPIPassword("secret") + require.False(t, ok) + require.Error(t, err) + + identity.valid = true + + ok, err = identity.VerifyAPIPassword("secret") + require.True(t, ok) + require.NoError(t, err) + + identity.user.Auth.API.Userpass.Enable = false + + ok, err = identity.VerifyAPIPassword("secret") + require.False(t, ok) + require.Error(t, err) + + identity.user.Auth.API.Userpass.Enable = true + identity.user.Auth.API.Userpass.Password = "terces" + + ok, err = identity.VerifyAPIPassword("secret") + require.False(t, ok) + require.NoError(t, err) +} + +func TestIdentityServiceBasicAuth(t *testing.T) { + user := User{ + Name: "foobar", + } + + identity := user.marshalIdentity() + + ok, err := identity.VerifyServiceBasicAuth("secret") + require.False(t, ok) + require.Error(t, err) + + identity.user.Auth.Services.Basic.Enable = true + identity.user.Auth.Services.Basic.Password = "secret" + + ok, err = identity.VerifyServiceBasicAuth("secret") + require.False(t, ok) + require.Error(t, err) + + identity.valid = true + + ok, err = identity.VerifyServiceBasicAuth("secret") + require.True(t, ok) + require.NoError(t, err) + + identity.user.Auth.Services.Basic.Enable = false + + ok, err = identity.VerifyServiceBasicAuth("secret") + require.False(t, ok) + require.Error(t, err) + + identity.user.Auth.Services.Basic.Enable = true + identity.user.Auth.Services.Basic.Password = "terces" + + ok, err = identity.VerifyServiceBasicAuth("secret") + require.False(t, ok) + require.NoError(t, err) + + password := identity.GetServiceBasicAuth() + require.Equal(t, "terces", password) +} + +func TestIdentityServiceTokenAuth(t *testing.T) { + user := User{ + Name: "foobar", + } + + identity := user.marshalIdentity() + + ok, err := identity.VerifyServiceToken("secret") + require.False(t, ok) + require.Error(t, err) + + identity.user.Auth.Services.Token = []string{"secret"} + + ok, err = identity.VerifyServiceToken("secret") + require.False(t, ok) + require.Error(t, err) + + identity.valid = true + + ok, err = identity.VerifyServiceToken("secret") + require.True(t, ok) + require.NoError(t, err) + + identity.user.Auth.Services.Token = []string{"terces"} + + ok, err = identity.VerifyServiceToken("secret") + require.False(t, ok) + require.NoError(t, err) + + token := identity.GetServiceToken() + require.Equal(t, "foobar:terces", token) +} + +func TestJWT(t *testing.T) { + dummyfs, err := fs.NewMemFilesystem(fs.MemConfig{}) + require.NoError(t, err) + + im, err := NewIdentityManager(IdentityConfig{ + FS: dummyfs, + 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) { + dummyfs, err := fs.NewMemFilesystem(fs.MemConfig{}) + require.NoError(t, err) + + im, err := NewIdentityManager(IdentityConfig{ + FS: dummyfs, + Superuser: User{Name: "foobar"}, + 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"}) + require.NoError(t, err) + + err = im.Create(User{Name: "foobaz"}) + require.Error(t, err) +} + +func TestCreateUserAuth0(t *testing.T) { + dummyfs, err := fs.NewMemFilesystem(fs.MemConfig{}) + require.NoError(t, err) + + im, err := NewIdentityManager(IdentityConfig{ + FS: dummyfs, + 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{ + Enable: true, + 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{ + Enable: true, + 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.GetVerifierByAuth0("foobaz") + require.Error(t, err) + require.Nil(t, identity) + + identity, err = im.GetVerifierByAuth0("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{ + Enable: true, + 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{ + Enable: true, + 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) { + dummyfs, err := fs.NewMemFilesystem(fs.MemConfig{}) + require.NoError(t, err) + + im, err := NewIdentityManager(IdentityConfig{ + FS: dummyfs, + 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) + + identity, err := im.GetVerifier("foobaz") + require.NoError(t, err) + require.NotNil(t, identity) + + err = im.Save() + require.NoError(t, err) + + im, err = NewIdentityManager(IdentityConfig{ + FS: dummyfs, + 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) +} + +func TestUpdateUser(t *testing.T) { + dummyfs, err := fs.NewMemFilesystem(fs.MemConfig{}) + require.NoError(t, err) + + im, err := NewIdentityManager(IdentityConfig{ + FS: dummyfs, + 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 TestUpdateUserAuth0(t *testing.T) { + dummyfs, err := fs.NewMemFilesystem(fs.MemConfig{}) + require.NoError(t, err) + + im, err := NewIdentityManager(IdentityConfig{ + FS: dummyfs, + 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{ + Enable: true, + 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.GetVerifierByAuth0("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.GetVerifierByAuth0("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) { + dummyfs, err := fs.NewMemFilesystem(fs.MemConfig{}) + require.NoError(t, err) + + im, err := NewIdentityManager(IdentityConfig{ + FS: dummyfs, + Superuser: User{Name: "foobar"}, + JWTRealm: "test-realm", + JWTSecret: "abc123", + Logger: nil, + }) + require.NoError(t, err) + require.NotNil(t, im) + + err = im.Remove("fooboz") + require.Error(t, err) + + err = im.Remove("foobar") + require.Error(t, err) + + err = im.Create(User{ + Name: "foobaz", + Superuser: false, + Auth: UserAuth{ + API: UserAuthAPI{ + Userpass: UserAuthPassword{ + Enable: true, + Password: "apisecret", + }, + Auth0: UserAuthAPIAuth0{}, + }, + Services: UserAuthServices{ + Basic: UserAuthPassword{ + Enable: true, + Password: "secret", + }, + Token: []string{"tokensecret"}, + }, + }, + }) + 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) + + 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.Remove("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, 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) { + dummyfs, err := fs.NewMemFilesystem(fs.MemConfig{}) + require.NoError(t, err) + + im, err := NewIdentityManager(IdentityConfig{ + FS: dummyfs, + 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{ + Enable: true, + 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{ + Enable: true, + 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.Remove("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.Remove("fooboz") + require.NoError(t, err) + + require.Equal(t, 0, len(manager.tenants)) + require.ElementsMatch(t, []string{ + "localjwt", + }, im.Validators()) +} + +func TestAutosave(t *testing.T) { + dummyfs, err := fs.NewMemFilesystem(fs.MemConfig{}) + require.NoError(t, err) + + im, err := NewIdentityManager(IdentityConfig{ + FS: dummyfs, + 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) + + im.Autosave(true) + + 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.Remove("fooboz") + require.NoError(t, err) + + data, err = dummyfs.ReadFile("./users.json") + require.NoError(t, err) + require.Equal(t, []byte("[]"), data) +}