From bf211c21f1a372f657f137e32d56ff0addd79261 Mon Sep 17 00:00:00 2001 From: Yuri Shangaraev Date: Thu, 19 Dec 2024 19:24:14 +0500 Subject: [PATCH 01/16] added password hasher --- api/admin/cli/main.go | 3 +- api/routing_fuzzing_test.go | 9 +++- application/application_test.go | 13 +++-- data/keycloak_user.go | 58 +++++++++++++++++----- data/keycloak_user_test.go | 21 +++++--- data/realm.go | 1 + data/user.go | 5 +- managers/files/manager.go | 3 +- managers/files/manager_test.go | 5 +- managers/redis/manager_realm_operations.go | 2 + managers/redis/manager_test.go | 27 ++++++---- managers/redis/manager_user_operations.go | 11 ++-- services/token_based_security.go | 15 ++++-- utils/hasher/b64hasher.go | 53 ++++++++++++++++++++ utils/hasher/b64hasher_test.go | 22 ++++++++ 15 files changed, 198 insertions(+), 50 deletions(-) create mode 100644 utils/hasher/b64hasher.go create mode 100644 utils/hasher/b64hasher_test.go diff --git a/api/admin/cli/main.go b/api/admin/cli/main.go index 897387c..f62af1b 100644 --- a/api/admin/cli/main.go +++ b/api/admin/cli/main.go @@ -6,9 +6,10 @@ import ( "encoding/json" "flag" "fmt" - "github.com/wissance/Ferrum/managers" "log" + "github.com/wissance/Ferrum/managers" + "github.com/wissance/Ferrum/api/admin/cli/operations" "github.com/wissance/Ferrum/config" "github.com/wissance/Ferrum/data" diff --git a/api/routing_fuzzing_test.go b/api/routing_fuzzing_test.go index fe333ca..5eb59ba 100644 --- a/api/routing_fuzzing_test.go +++ b/api/routing_fuzzing_test.go @@ -14,6 +14,7 @@ import ( "github.com/wissance/Ferrum/config" "github.com/wissance/Ferrum/data" "github.com/wissance/Ferrum/dto" + b64hasher "github.com/wissance/Ferrum/utils/hasher" sf "github.com/wissance/stringFormatter" "github.com/stretchr/testify/assert" @@ -29,6 +30,8 @@ const ( ) var ( + testSalt = "salt" + hashedPassword = b64hasher.HashPassword("1234567890", testSalt) testKey = []byte("qwerty1234567890") testServerData = data.ServerData{ Realms: []data.Realm{ @@ -39,16 +42,18 @@ var ( Type: data.ClientIdAndSecrets, Value: testClient1Secret, }}, - }, Users: []interface{}{ + }, + Users: []interface{}{ map[string]interface{}{ "info": map[string]interface{}{ "sub": "667ff6a7-3f6b-449b-a217-6fc5d9ac0723", "name": "vano", "preferred_username": "vano", "given_name": "vano ivanov", "family_name": "ivanov", "email_verified": true, }, - "credentials": map[string]interface{}{"password": "1234567890"}, + "credentials": map[string]interface{}{"password": hashedPassword}, }, }, + PasswordSalt: testSalt, }, }, } diff --git a/application/application_test.go b/application/application_test.go index 7faec27..09a14a9 100644 --- a/application/application_test.go +++ b/application/application_test.go @@ -17,6 +17,7 @@ import ( "github.com/wissance/Ferrum/data" "github.com/wissance/Ferrum/dto" "github.com/wissance/Ferrum/errors" + b64hasher "github.com/wissance/Ferrum/utils/hasher" "github.com/wissance/stringFormatter" ) @@ -29,8 +30,10 @@ const ( ) var ( - testKey = []byte("qwerty1234567890") - testServerData = data.ServerData{ + testSalt = "salt" + testHashedPassowrd = b64hasher.HashPassword("1234567890", testSalt) + testKey = []byte("qwerty1234567890") + testServerData = data.ServerData{ Realms: []data.Realm{ { Name: testRealm1, TokenExpiration: testAccessTokenExpiration, RefreshTokenExpiration: testRefreshTokenExpiration, @@ -39,16 +42,18 @@ var ( Type: data.ClientIdAndSecrets, Value: testClient1Secret, }}, - }, Users: []interface{}{ + }, + Users: []interface{}{ map[string]interface{}{ "info": map[string]interface{}{ "sub": "667ff6a7-3f6b-449b-a217-6fc5d9ac0723", "name": "vano", "preferred_username": "vano", "given_name": "vano ivanov", "family_name": "ivanov", "email_verified": true, }, - "credentials": map[string]interface{}{"password": "1234567890"}, + "credentials": map[string]interface{}{"password": testHashedPassowrd}, }, }, + PasswordSalt: testSalt, }, }, } diff --git a/data/keycloak_user.go b/data/keycloak_user.go index 6f57de7..89c748e 100644 --- a/data/keycloak_user.go +++ b/data/keycloak_user.go @@ -6,6 +6,7 @@ import ( "github.com/google/uuid" "github.com/ohler55/ojg/jp" + b64hasher "github.com/wissance/Ferrum/utils/hasher" ) const ( @@ -42,23 +43,36 @@ func (user *KeyCloakUser) GetUsername() string { return getPathStringValue[string](user.rawData, "info.preferred_username") } -// GetPassword returns password -/* this function use internal map to navigate over credentials.password keys to retrieve a password +// GetPasswordHash returns hash of password +/* this function use internal map to navigate over credentials.password keys to retrieve a hash of password * Parameters: no - * Returns: password + * Returns: hash of password */ -// todo(UMV): this function should be changed to GetPasswordHash also we should consider case when User is External -func (user *KeyCloakUser) GetPassword() string { - return getPathStringValue[string](user.rawData, pathToPassword) +// todo(UMV): we should consider case when User is External +func (user *KeyCloakUser) GetPasswordHash() string { + password := getPathStringValue[string](user.rawData, pathToPassword) + if !b64hasher.IsPasswordHashed(password) { + // todo (YuriShang): think about actions if the password is not hashed + } + return password } -func (user *KeyCloakUser) SetPassword(password string) error { - mask, err := jp.ParseString(pathToPassword) - if err != nil { - return fmt.Errorf("jp.ParseString failed: %w", err) - } - if err := mask.Set(user.rawData, password); err != nil { - return fmt.Errorf("jp.Set failed: %w", err) +// HashPassword +/* this function changes a raw password to its hash in the user's rawData and jsonRawData + * Parameters: salt - salt for make password hash more strong + */ +func (user *KeyCloakUser) HashPassword(salt string) { + password := getPathStringValue[string](user.rawData, pathToPassword) + hashedPassword := b64hasher.HashPassword(password, salt) + setPathStringValue(user.rawData, pathToPassword, hashedPassword) + jsonData, _ := json.Marshal(&user.rawData) + user.jsonRawData = string(jsonData) +} + +func (user *KeyCloakUser) SetPassword(password, salt string) error { + hashedPassword := b64hasher.HashPassword(password, salt) + if err := setPathStringValue(user.rawData, pathToPassword, hashedPassword); err != nil { + return err } jsonData, _ := json.Marshal(user.rawData) user.jsonRawData = string(jsonData) @@ -131,3 +145,21 @@ func getPathStringValue[T any](rawData interface{}, path string) T { } return result } + +// setPathStringValue is a function to search data by path and set data by key, key represents as a jsonpath navigation property +/* this function uses json path to navigate over nested maps and set data + * Parameters: + * - rawData - json object + * - path - json path to retrieve part of json + * - value - value to be set to rawData + */ +func setPathStringValue(rawData interface{}, path string, value string) error { + mask, err := jp.ParseString(path) + if err != nil { + return fmt.Errorf("jp.ParseString failed: %w", err) + } + if err := mask.Set(rawData, value); err != nil { + return fmt.Errorf("jp.Set failed: %w", err) + } + return nil +} diff --git a/data/keycloak_user_test.go b/data/keycloak_user_test.go index 18f9055..7f3e3ad 100644 --- a/data/keycloak_user_test.go +++ b/data/keycloak_user_test.go @@ -2,9 +2,10 @@ package data import ( "encoding/json" + "testing" + "github.com/stretchr/testify/assert" sf "github.com/wissance/stringFormatter" - "testing" ) func TestInitUserWithJsonAndCheck(t *testing.T) { @@ -16,12 +17,18 @@ func TestInitUserWithJsonAndCheck(t *testing.T) { userTemplate string federationId string }{ - {name: "simple_user", userName: "admin", preferredUsername: "Administrator", isFederated: false, - userTemplate: `{"info":{"name":"{0}", "preferred_username": "{1}"}}`}, - {name: "federated_user", userName: `m.ushakov`, preferredUsername: "m.ushakov", isFederated: true, federationId: "Wissance_test_domain", - userTemplate: `{"info":{"name":"{0}", "preferred_username": "{1}"}, "federation":{"name":"Wissance_test_domain"}}`}, - {name: "federated_user", userName: `root`, preferredUsername: "root", isFederated: false, - userTemplate: `{"info":{"name":"{0}", "preferred_username": "{1}"}, "federation":{"cfg":{}}}`}, + { + name: "simple_user", userName: "admin", preferredUsername: "Administrator", isFederated: false, + userTemplate: `{"info":{"name":"{0}", "preferred_username": "{1}"}}`, + }, + { + name: "federated_user", userName: `m.ushakov`, preferredUsername: "m.ushakov", isFederated: true, federationId: "Wissance_test_domain", + userTemplate: `{"info":{"name":"{0}", "preferred_username": "{1}"}, "federation":{"name":"Wissance_test_domain"}}`, + }, + { + name: "federated_user", userName: `root`, preferredUsername: "root", isFederated: false, + userTemplate: `{"info":{"name":"{0}", "preferred_username": "{1}"}, "federation":{"cfg":{}}}`, + }, } for _, tCase := range testCases { diff --git a/data/realm.go b/data/realm.go index 71721ff..269ce24 100644 --- a/data/realm.go +++ b/data/realm.go @@ -12,4 +12,5 @@ type Realm struct { TokenExpiration int `json:"token_expiration"` RefreshTokenExpiration int `json:"refresh_expiration"` UserFederationServices []UserFederationServiceConfig `json:"user_federation_services"` + PasswordSalt string } diff --git a/data/user.go b/data/user.go index 228ee93..5149466 100644 --- a/data/user.go +++ b/data/user.go @@ -6,8 +6,9 @@ import "github.com/google/uuid" // because Password is not an only method for authentication type User interface { GetUsername() string - GetPassword() string - SetPassword(password string) error + GetPasswordHash() string + SetPassword(password, salt string) error + HashPassword(salt string) GetId() uuid.UUID GetUserInfo() interface{} GetRawData() interface{} diff --git a/managers/files/manager.go b/managers/files/manager.go index e28fc58..4feda15 100644 --- a/managers/files/manager.go +++ b/managers/files/manager.go @@ -2,9 +2,10 @@ package files import ( "encoding/json" - "github.com/wissance/Ferrum/config" "os" + "github.com/wissance/Ferrum/config" + "github.com/wissance/Ferrum/errors" "github.com/google/uuid" diff --git a/managers/files/manager_test.go b/managers/files/manager_test.go index aa2fa49..48d107c 100644 --- a/managers/files/manager_test.go +++ b/managers/files/manager_test.go @@ -2,13 +2,14 @@ package files import ( "encoding/json" + "testing" + "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/wissance/Ferrum/config" "github.com/wissance/Ferrum/data" "github.com/wissance/Ferrum/logging" - "testing" ) const testDataFile = "test_data.json" @@ -160,5 +161,5 @@ func checkUsers(t *testing.T, expected *[]data.User, actual *[]data.User) { func checkUser(t *testing.T, expected *data.User, actual *data.User) { assert.Equal(t, (*expected).GetId(), (*actual).GetId()) assert.Equal(t, (*expected).GetUsername(), (*actual).GetUsername()) - assert.Equal(t, (*expected).GetPassword(), (*actual).GetPassword()) + assert.Equal(t, (*expected).GetPasswordHash(), (*actual).GetPasswordHash()) } diff --git a/managers/redis/manager_realm_operations.go b/managers/redis/manager_realm_operations.go index dce169c..8f9752e 100755 --- a/managers/redis/manager_realm_operations.go +++ b/managers/redis/manager_realm_operations.go @@ -7,6 +7,7 @@ import ( "github.com/wissance/Ferrum/config" "github.com/wissance/Ferrum/data" appErrs "github.com/wissance/Ferrum/errors" + b64hasher "github.com/wissance/Ferrum/utils/hasher" sf "github.com/wissance/stringFormatter" ) @@ -123,6 +124,7 @@ func (mn *RedisDataManager) CreateRealm(newRealm data.Realm) error { Users: []any{}, TokenExpiration: newRealm.TokenExpiration, RefreshTokenExpiration: newRealm.RefreshTokenExpiration, + PasswordSalt: b64hasher.GenerateSalt(), } jsonShortRealm, err := json.Marshal(shortRealm) if err != nil { diff --git a/managers/redis/manager_test.go b/managers/redis/manager_test.go index 73f3f60..6de8914 100644 --- a/managers/redis/manager_test.go +++ b/managers/redis/manager_test.go @@ -461,12 +461,14 @@ func TestGetClientFailsNonExistingClient(t *testing.T) { func TestGetUsersSuccessfully(t *testing.T) { // 1. Create Realm manager := createTestRedisDataManager(t) - realm := data.Realm{ + r := data.Realm{ Name: sf.Format("realm_4_get_multiple_users_{0}", uuid.New().String()), TokenExpiration: 3600, RefreshTokenExpiration: 1800, } - err := manager.CreateRealm(realm) + err := manager.CreateRealm(r) + assert.NoError(t, err) + realm, err := manager.getRealmObject(r.Name) assert.NoError(t, err) // 2. Create multiple users users := make([]data.User, 3) @@ -479,8 +481,8 @@ func TestGetUsersSuccessfully(t *testing.T) { err = json.Unmarshal([]byte(jsonStr), &rawUser) assert.NoError(t, err) user := data.CreateUser(rawUser) - users[i] = user err = manager.CreateUser(realm.Name, user) + users[i] = user assert.NoError(t, err) } // 3. Get all related to realm users @@ -792,19 +794,21 @@ func TestChangeUserPasswordSuccessfully(t *testing.T) { Value: uuid.New().String(), }, } - realm.Clients = append([]data.Client{client}) + realm.Clients = append(realm.Clients, client) + err := manager.CreateRealm(realm) + assert.NoError(t, err) + + createdRealm, err := manager.getRealmObject(realm.Name) + assert.NoError(t, err) userName := "new_app_user" userTemplate := `{"info":{"preferred_username":"{0}"}, "credentials":{"password": "{1}"}}` userJson := sf.Format(userTemplate, userName, "123") var rawUser interface{} - err := json.Unmarshal([]byte(userJson), &rawUser) - assert.NoError(t, err) - realm.Users = append([]interface{}{rawUser}) - - err = manager.CreateRealm(realm) + err = json.Unmarshal([]byte(userJson), &rawUser) assert.NoError(t, err) - _, err = manager.GetRealm(realm.Name) + user := data.CreateUser(rawUser) + err = manager.CreateUser(realm.Name, user) assert.NoError(t, err) // 2. Reset Password and check ... @@ -816,6 +820,7 @@ func TestChangeUserPasswordSuccessfully(t *testing.T) { err = json.Unmarshal([]byte(userJson), &rawUser) assert.NoError(t, err) expectedUser := data.CreateUser(rawUser) + expectedUser.HashPassword(createdRealm.PasswordSalt) u, err := manager.GetUser(realm.Name, userName) assert.NoError(t, err) checkUser(t, &expectedUser, &u) @@ -1057,7 +1062,7 @@ func checkUsers(t *testing.T, expected *[]data.User, actual *[]data.User) { func checkUser(t *testing.T, expected *data.User, actual *data.User) { assert.Equal(t, (*expected).GetId(), (*actual).GetId()) assert.Equal(t, (*expected).GetUsername(), (*actual).GetUsername()) - assert.Equal(t, (*expected).GetPassword(), (*actual).GetPassword()) + assert.Equal(t, (*expected).GetPasswordHash(), (*actual).GetPasswordHash()) } func checkUserFederationConfigs(t *testing.T, expected *[]data.UserFederationServiceConfig, actual *[]data.UserFederationServiceConfig) { diff --git a/managers/redis/manager_user_operations.go b/managers/redis/manager_user_operations.go index d3fec05..46b7f80 100644 --- a/managers/redis/manager_user_operations.go +++ b/managers/redis/manager_user_operations.go @@ -3,6 +3,7 @@ package redis import ( "encoding/json" "errors" + "github.com/google/uuid" "github.com/wissance/Ferrum/config" "github.com/wissance/Ferrum/data" @@ -128,7 +129,7 @@ func (mn *RedisDataManager) CreateUser(realmName string, userNew data.User) erro } // TODO(SIA) Add transaction // TODO(SIA) use function isExists - _, err := mn.getRealmObject(realmName) + realm, err := mn.getRealmObject(realmName) if err != nil { mn.logger.Warn(sf.Format("CreateUser: GetRealmObject failed, error: {0}", err.Error())) return err @@ -143,7 +144,7 @@ func (mn *RedisDataManager) CreateUser(realmName string, userNew data.User) erro mn.logger.Warn(sf.Format("CreateUser: GetUser failed, error: {0}", err.Error())) return err } - + userNew.HashPassword(realm.PasswordSalt) upsertUserErr := mn.upsertUserObject(realmName, userName, userNew.GetJsonString()) if upsertUserErr != nil { mn.logger.Error(sf.Format("CreateUser: addUserToRealm failed, error: {0}", upsertUserErr.Error())) @@ -241,7 +242,11 @@ func (mn *RedisDataManager) SetPassword(realmName string, userName string, passw if err != nil { return errors2.NewUnknownError("GetUser", "RedisDataManager.SetPassword", err) } - if setPasswordErr := user.SetPassword(password); setPasswordErr != nil { + realm, err := mn.getRealmObject(realmName) + if err != nil { + return errors2.NewUnknownError("getRealmObject", "RedisDataManager.SetPassword", err) + } + if setPasswordErr := user.SetPassword(password, realm.PasswordSalt); setPasswordErr != nil { return errors2.NewUnknownError("SetPassword", "RedisDataManager.SetPassword", setPasswordErr) } if upsertUserErr := mn.upsertUserObject(realmName, userName, user.GetJsonString()); upsertUserErr != nil { diff --git a/services/token_based_security.go b/services/token_based_security.go index ef4118d..2517b00 100644 --- a/services/token_based_security.go +++ b/services/token_based_security.go @@ -1,15 +1,17 @@ package services import ( - sf "github.com/wissance/stringFormatter" "time" + sf "github.com/wissance/stringFormatter" + "github.com/google/uuid" "github.com/wissance/Ferrum/data" "github.com/wissance/Ferrum/dto" "github.com/wissance/Ferrum/errors" "github.com/wissance/Ferrum/logging" "github.com/wissance/Ferrum/managers" + b64hasher "github.com/wissance/Ferrum/utils/hasher" ) // TokenBasedSecurityService structure that implements SecurityService @@ -73,15 +75,20 @@ func (service *TokenBasedSecurityService) CheckCredentials(tokenIssueData *dto.T return &data.OperationError{Msg: errors.InvalidUserCredentialsMsg, Description: errors.InvalidUserCredentialsDesc} } + realm, err := (*service.DataProvider).GetRealm(realmName) + if err != nil { + service.logger.Trace("Credential check: failed to get realm") + return &data.OperationError{Msg: "failed to get realm", Description: err.Error()} + } + if user.IsFederatedUser() { msg := sf.Format("User \"{0}\" configured as federated, currently it is not fully supported, wait for future releases", user.GetUsername()) service.logger.Warn(msg) return &data.OperationError{Msg: "federated user not supported", Description: msg} } else { - // todo(UMV): use hash instead raw passwords - password := user.GetPassword() - if password != tokenIssueData.Password { + oldPasswordHash := user.GetPasswordHash() + if !b64hasher.IsPasswordsMatch(tokenIssueData.Password, realm.PasswordSalt, oldPasswordHash) { service.logger.Trace("Credential check: password mismatch") return &data.OperationError{Msg: errors.InvalidUserCredentialsMsg, Description: errors.InvalidUserCredentialsDesc} } diff --git a/utils/hasher/b64hasher.go b/utils/hasher/b64hasher.go new file mode 100644 index 0000000..5e2ff1a --- /dev/null +++ b/utils/hasher/b64hasher.go @@ -0,0 +1,53 @@ +package b64hasher + +import ( + "crypto/sha512" + "encoding/base64" + "math/rand" +) + +func GenerateSalt() string { + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+=" + salt := make([]byte, 32) + for i := range salt { + salt[i] = charset[rand.Intn(len(charset))] + } + return string(salt) +} + +func HashPassword(password, salt string) string { + passwordBytes := []byte(password + salt) + + sha512Hasher := sha512.New() + sha512Hasher.Write(passwordBytes) + + hashedPasswordBytes := sha512Hasher.Sum(nil) + b64encoded := b64Encode(hashedPasswordBytes) + return b64encoded +} + +func IsPasswordsMatch(password, salt, hash string) bool { + currPasswordHash := HashPassword(password, salt) + return b64Decode(hash) == b64Decode(currPasswordHash) +} + +func IsPasswordHashed(password string) bool { + decoded := b64Decode(password) + if len(decoded) == 0 { + return false + } + return true +} + +func b64Encode(encoded []byte) string { + cstr := base64.URLEncoding.EncodeToString(encoded) + return cstr +} + +func b64Decode(encoded string) string { + cstr, err := base64.URLEncoding.DecodeString(encoded) + if err != nil { + return "" + } + return string(cstr) +} diff --git a/utils/hasher/b64hasher_test.go b/utils/hasher/b64hasher_test.go new file mode 100644 index 0000000..c0d72f9 --- /dev/null +++ b/utils/hasher/b64hasher_test.go @@ -0,0 +1,22 @@ +package b64hasher + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_HashPassword(t *testing.T) { + t.Run("success", func(t *testing.T) { + // Arrange + pwd := "qwerty" + salt := "salt" + + // Act + hashedPwd := HashPassword(pwd, salt) + isMatch := IsPasswordsMatch(pwd, salt, hashedPwd) + + // Assert + assert.True(t, isMatch) + }) +} From ac298e0c553dcb76ebe002a9821073180dd54dd8 Mon Sep 17 00:00:00 2001 From: Yuri Shangaraev Date: Fri, 20 Dec 2024 09:20:06 +0500 Subject: [PATCH 02/16] added password hashing check to HashPassword method --- data/keycloak_user.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/data/keycloak_user.go b/data/keycloak_user.go index 89c748e..cb054ab 100644 --- a/data/keycloak_user.go +++ b/data/keycloak_user.go @@ -63,10 +63,12 @@ func (user *KeyCloakUser) GetPasswordHash() string { */ func (user *KeyCloakUser) HashPassword(salt string) { password := getPathStringValue[string](user.rawData, pathToPassword) - hashedPassword := b64hasher.HashPassword(password, salt) - setPathStringValue(user.rawData, pathToPassword, hashedPassword) - jsonData, _ := json.Marshal(&user.rawData) - user.jsonRawData = string(jsonData) + if !b64hasher.IsPasswordHashed(password) { + hashedPassword := b64hasher.HashPassword(password, salt) + setPathStringValue(user.rawData, pathToPassword, hashedPassword) + jsonData, _ := json.Marshal(&user.rawData) + user.jsonRawData = string(jsonData) + } } func (user *KeyCloakUser) SetPassword(password, salt string) error { From 004b277fb90d98bc6e63ea3f224bf1d84146a33c Mon Sep 17 00:00:00 2001 From: Yuri Shangaraev Date: Wed, 8 Jan 2025 23:41:49 +0500 Subject: [PATCH 03/16] implemented PasswordJsonEncoder --- api/admin/cli/main.go | 18 +++++++- api/routing_fuzzing_test.go | 13 +++--- application/application_test.go | 5 +- data/keycloak_user.go | 34 ++++++-------- data/keycloak_user_test.go | 4 +- data/realm.go | 5 +- data/user.go | 8 ++-- managers/files/manager.go | 5 +- managers/files/manager_test.go | 8 +++- managers/files/test_data.json | 3 +- managers/redis/manager_realm_operations.go | 13 ++++-- managers/redis/manager_test.go | 42 ++++++++++------- managers/redis/manager_user_operations.go | 21 ++++++--- services/token_based_security.go | 3 +- .../b64hasher.go => encoding/encoding.go} | 46 +++++++++++++------ .../encoding_test.go} | 7 +-- 16 files changed, 148 insertions(+), 87 deletions(-) rename utils/{hasher/b64hasher.go => encoding/encoding.go} (54%) rename utils/{hasher/b64hasher_test.go => encoding/encoding_test.go} (61%) diff --git a/api/admin/cli/main.go b/api/admin/cli/main.go index f62af1b..410ec52 100644 --- a/api/admin/cli/main.go +++ b/api/admin/cli/main.go @@ -122,7 +122,14 @@ func main() { if err := json.Unmarshal(value, &userNew); err != nil { log.Fatalf("json.Unmarshal failed: %s", err) } - user := data.CreateUser(userNew) + if resourceId == "" { + log.Fatalf("Realm name not specified") + } + realm, err := manager.GetRealm(resourceId) + if err != nil { + log.Fatalf("GetRealm failed: %s", err) + } + user := data.CreateUser(userNew, realm.Encoder) if err := manager.CreateUser(params, user); err != nil { log.Fatalf("CreateUser failed: %s", err) } @@ -203,7 +210,14 @@ func main() { if err := json.Unmarshal(value, &newUser); err != nil { log.Fatalf("json.Unmarshal failed: %s", err) } - user := data.CreateUser(newUser) + if resourceId == "" { + log.Fatalf("Realm name not specified") + } + realm, err := manager.GetRealm(resourceId) + if err != nil { + log.Fatalf("GetRealm failed: %s", err) + } + user := data.CreateUser(newUser, realm.Encoder) if err := manager.UpdateUser(params, resourceId, user); err != nil { log.Fatalf("UpdateUser failed: %s", err) } diff --git a/api/routing_fuzzing_test.go b/api/routing_fuzzing_test.go index 5eb59ba..2e7c717 100644 --- a/api/routing_fuzzing_test.go +++ b/api/routing_fuzzing_test.go @@ -14,7 +14,7 @@ import ( "github.com/wissance/Ferrum/config" "github.com/wissance/Ferrum/data" "github.com/wissance/Ferrum/dto" - b64hasher "github.com/wissance/Ferrum/utils/hasher" + "github.com/wissance/Ferrum/utils/encoding" sf "github.com/wissance/stringFormatter" "github.com/stretchr/testify/assert" @@ -30,10 +30,11 @@ const ( ) var ( - testSalt = "salt" - hashedPassword = b64hasher.HashPassword("1234567890", testSalt) - testKey = []byte("qwerty1234567890") - testServerData = data.ServerData{ + testSalt = "salt" + encoder = encoding.NewPasswordJsonEncoder(testSalt) + testHashedPassowrd = encoder.HashPassword("1234567890") + testKey = []byte("qwerty1234567890") + testServerData = data.ServerData{ Realms: []data.Realm{ { Name: testRealm1, TokenExpiration: testAccessTokenExpiration, RefreshTokenExpiration: testRefreshTokenExpiration, @@ -50,7 +51,7 @@ var ( "name": "vano", "preferred_username": "vano", "given_name": "vano ivanov", "family_name": "ivanov", "email_verified": true, }, - "credentials": map[string]interface{}{"password": hashedPassword}, + "credentials": map[string]interface{}{"password": testHashedPassowrd}, }, }, PasswordSalt: testSalt, diff --git a/application/application_test.go b/application/application_test.go index 09a14a9..7138db1 100644 --- a/application/application_test.go +++ b/application/application_test.go @@ -17,7 +17,7 @@ import ( "github.com/wissance/Ferrum/data" "github.com/wissance/Ferrum/dto" "github.com/wissance/Ferrum/errors" - b64hasher "github.com/wissance/Ferrum/utils/hasher" + "github.com/wissance/Ferrum/utils/encoding" "github.com/wissance/stringFormatter" ) @@ -31,7 +31,8 @@ const ( var ( testSalt = "salt" - testHashedPassowrd = b64hasher.HashPassword("1234567890", testSalt) + encoder = encoding.NewPasswordJsonEncoder(testSalt) + testHashedPassowrd = encoder.HashPassword("1234567890") testKey = []byte("qwerty1234567890") testServerData = data.ServerData{ Realms: []data.Realm{ diff --git a/data/keycloak_user.go b/data/keycloak_user.go index cb054ab..d5c4409 100644 --- a/data/keycloak_user.go +++ b/data/keycloak_user.go @@ -6,7 +6,7 @@ import ( "github.com/google/uuid" "github.com/ohler55/ojg/jp" - b64hasher "github.com/wissance/Ferrum/utils/hasher" + "github.com/wissance/Ferrum/utils/encoding" ) const ( @@ -26,9 +26,11 @@ type KeyCloakUser struct { * - rawData - any json * Return: instance of User as KeyCloakUser */ -func CreateUser(rawData interface{}) User { +func CreateUser(rawData interface{}, encoder encoding.PasswordJsonEncoder) User { jsonData, _ := json.Marshal(&rawData) kcUser := &KeyCloakUser{rawData: rawData, jsonRawData: string(jsonData)} + password := getPathStringValue[string](kcUser.rawData, pathToPassword) + kcUser.SetPassword(password, encoder) user := User(kcUser) return user } @@ -51,32 +53,24 @@ func (user *KeyCloakUser) GetUsername() string { // todo(UMV): we should consider case when User is External func (user *KeyCloakUser) GetPasswordHash() string { password := getPathStringValue[string](user.rawData, pathToPassword) - if !b64hasher.IsPasswordHashed(password) { + if !encoding.IsPasswordHashed(password) { // todo (YuriShang): think about actions if the password is not hashed } return password } -// HashPassword -/* this function changes a raw password to its hash in the user's rawData and jsonRawData - * Parameters: salt - salt for make password hash more strong +// SetPassword +/* this function changes a raw password to its hash in the user's rawData and jsonRawData and sets it + * Parameters: + * - password - new password + * - encoder - encoder object with salt and hasher */ -func (user *KeyCloakUser) HashPassword(salt string) { - password := getPathStringValue[string](user.rawData, pathToPassword) - if !b64hasher.IsPasswordHashed(password) { - hashedPassword := b64hasher.HashPassword(password, salt) - setPathStringValue(user.rawData, pathToPassword, hashedPassword) - jsonData, _ := json.Marshal(&user.rawData) - user.jsonRawData = string(jsonData) - } -} - -func (user *KeyCloakUser) SetPassword(password, salt string) error { - hashedPassword := b64hasher.HashPassword(password, salt) - if err := setPathStringValue(user.rawData, pathToPassword, hashedPassword); err != nil { +func (user *KeyCloakUser) SetPassword(password string, encoder encoding.PasswordJsonEncoder) error { + hashed := encoder.HashPassword(password) + if err := setPathStringValue(user.rawData, pathToPassword, hashed); err != nil { return err } - jsonData, _ := json.Marshal(user.rawData) + jsonData, _ := json.Marshal(&user.rawData) user.jsonRawData = string(jsonData) return nil } diff --git a/data/keycloak_user_test.go b/data/keycloak_user_test.go index 7f3e3ad..510a121 100644 --- a/data/keycloak_user_test.go +++ b/data/keycloak_user_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/wissance/Ferrum/utils/encoding" sf "github.com/wissance/stringFormatter" ) @@ -38,7 +39,8 @@ func TestInitUserWithJsonAndCheck(t *testing.T) { var rawUserData interface{} err := json.Unmarshal([]byte(jsonStr), &rawUserData) assert.NoError(t, err) - user := CreateUser(rawUserData) + encoder := encoding.NewPasswordJsonEncoder("salt") + user := CreateUser(rawUserData, encoder) assert.Equal(t, tCase.preferredUsername, user.GetUsername()) assert.Equal(t, tCase.isFederated, user.IsFederatedUser()) if user.IsFederatedUser() { diff --git a/data/realm.go b/data/realm.go index 269ce24..3317127 100644 --- a/data/realm.go +++ b/data/realm.go @@ -1,5 +1,7 @@ package data +import "github.com/wissance/Ferrum/utils/encoding" + // Realm is a struct that describes typical Realm /* It was originally designed to efficiently work in memory with small amount of data therefore it contains relations with Clients and Users * But in a systems with thousands of users working at the same time it is too expensive to fetch Realm with all relations therefore @@ -12,5 +14,6 @@ type Realm struct { TokenExpiration int `json:"token_expiration"` RefreshTokenExpiration int `json:"refresh_expiration"` UserFederationServices []UserFederationServiceConfig `json:"user_federation_services"` - PasswordSalt string + PasswordSalt string `json:"password_salt"` + Encoder encoding.PasswordJsonEncoder } diff --git a/data/user.go b/data/user.go index 5149466..7b49f2d 100644 --- a/data/user.go +++ b/data/user.go @@ -1,14 +1,16 @@ package data -import "github.com/google/uuid" +import ( + "github.com/google/uuid" + "github.com/wissance/Ferrum/utils/encoding" +) // User is a common user interface with all Required methods to get information about user, in future we probably won't have GetPassword method // because Password is not an only method for authentication type User interface { GetUsername() string GetPasswordHash() string - SetPassword(password, salt string) error - HashPassword(salt string) + SetPassword(password string, encoder encoding.PasswordJsonEncoder) error GetId() uuid.UUID GetUserInfo() interface{} GetRawData() interface{} diff --git a/managers/files/manager.go b/managers/files/manager.go index 4feda15..c38a753 100644 --- a/managers/files/manager.go +++ b/managers/files/manager.go @@ -5,6 +5,7 @@ import ( "os" "github.com/wissance/Ferrum/config" + "github.com/wissance/Ferrum/utils/encoding" "github.com/wissance/Ferrum/errors" @@ -78,6 +79,7 @@ func (mn *FileDataManager) GetRealm(realmName string) (*data.Realm, error) { // case-sensitive comparison, myapp and MyApP are different realms if e.Name == realmName { e.Users = nil + e.Encoder = encoding.NewPasswordJsonEncoder(e.PasswordSalt) return &e, nil } } @@ -101,8 +103,9 @@ func (mn *FileDataManager) GetUsers(realmName string) ([]data.User, error) { return nil, errors.ErrZeroLength } users := make([]data.User, len(e.Users)) + e.Encoder = encoding.NewPasswordJsonEncoder(e.PasswordSalt) for i, u := range e.Users { - user := data.CreateUser(u) + user := data.CreateUser(u, e.Encoder) users[i] = user } return users, nil diff --git a/managers/files/manager_test.go b/managers/files/manager_test.go index 48d107c..c0d7806 100644 --- a/managers/files/manager_test.go +++ b/managers/files/manager_test.go @@ -68,7 +68,9 @@ func TestGetUserSuccessfully(t *testing.T) { var rawUser interface{} err := json.Unmarshal([]byte(userJson), &rawUser) assert.NoError(t, err) - expectedUser := data.CreateUser(rawUser) + r, err := manager.GetRealm(realm) + assert.NoError(t, err) + expectedUser := data.CreateUser(rawUser, r.Encoder) user, err := manager.GetUser(realm, userName) assert.NoError(t, err) checkUser(t, &expectedUser, &user) @@ -97,7 +99,9 @@ func TestGetUserByIdSuccessfully(t *testing.T) { var rawUser interface{} err := json.Unmarshal([]byte(userJson), &rawUser) assert.NoError(t, err) - expectedUser := data.CreateUser(rawUser) + r, err := manager.GetRealm(realm) + assert.NoError(t, err) + expectedUser := data.CreateUser(rawUser, r.Encoder) user, err := manager.GetUserById(realm, userId) assert.NoError(t, err) checkUser(t, &expectedUser, &user) diff --git a/managers/files/test_data.json b/managers/files/test_data.json index eb262a6..dda6580 100644 --- a/managers/files/test_data.json +++ b/managers/files/test_data.json @@ -49,7 +49,8 @@ "password": "qwerty_user" } } - ] + ], + "password_salt": "super_strong_salt" } ] } diff --git a/managers/redis/manager_realm_operations.go b/managers/redis/manager_realm_operations.go index 8f9752e..e5f3f1b 100755 --- a/managers/redis/manager_realm_operations.go +++ b/managers/redis/manager_realm_operations.go @@ -7,7 +7,7 @@ import ( "github.com/wissance/Ferrum/config" "github.com/wissance/Ferrum/data" appErrs "github.com/wissance/Ferrum/errors" - b64hasher "github.com/wissance/Ferrum/utils/hasher" + "github.com/wissance/Ferrum/utils/encoding" sf "github.com/wissance/stringFormatter" ) @@ -48,6 +48,7 @@ func (mn *RedisDataManager) GetRealm(realmName string) (*data.Realm, error) { } } realm.UserFederationServices = configs + realm.Encoder = encoding.NewPasswordJsonEncoder(realm.PasswordSalt) return realm, nil } @@ -70,7 +71,7 @@ func (mn *RedisDataManager) CreateRealm(newRealm data.Realm) error { } // TODO(SIA) Add transaction // TODO(SIA) use function isExists - _, err := mn.getRealmObject(newRealm.Name) + _, err := mn.GetRealm(newRealm.Name) if err == nil { return appErrs.NewObjectExistsError(string(Realm), newRealm.Name, "") } @@ -99,10 +100,13 @@ func (mn *RedisDataManager) CreateRealm(newRealm data.Realm) error { } } + salt := encoding.GenerateRandomSalt() + if len(newRealm.Users) != 0 { realmUsers := make([]data.ExtendedIdentifier, len(newRealm.Users)) + encoder := encoding.NewPasswordJsonEncoder(salt) for i, user := range newRealm.Users { - newUser := data.CreateUser(user) + newUser := data.CreateUser(user, encoder) newUserName := newUser.GetUsername() if upsertUserErr := mn.upsertUserObject(newRealm.Name, newUserName, newUser.GetJsonString()); upsertUserErr != nil { return appErrs.NewUnknownError("upsertUserObject", "RedisDataManager.CreateRealm", upsertUserErr) @@ -124,7 +128,8 @@ func (mn *RedisDataManager) CreateRealm(newRealm data.Realm) error { Users: []any{}, TokenExpiration: newRealm.TokenExpiration, RefreshTokenExpiration: newRealm.RefreshTokenExpiration, - PasswordSalt: b64hasher.GenerateSalt(), + PasswordSalt: salt, + Encoder: encoding.PasswordJsonEncoder{}, } jsonShortRealm, err := json.Marshal(shortRealm) if err != nil { diff --git a/managers/redis/manager_test.go b/managers/redis/manager_test.go index 6de8914..51722a1 100644 --- a/managers/redis/manager_test.go +++ b/managers/redis/manager_test.go @@ -12,6 +12,7 @@ import ( "github.com/wissance/Ferrum/data" appErrs "github.com/wissance/Ferrum/errors" "github.com/wissance/Ferrum/logging" + "github.com/wissance/Ferrum/utils/encoding" sf "github.com/wissance/stringFormatter" ) @@ -76,7 +77,7 @@ func TestCreateRealmSuccessfully(t *testing.T) { expectedUsers := make([]data.User, len(realm.Users)) if len(realm.Users) > 0 { for i := range realm.Users { - expectedUsers[i] = data.CreateUser(realm.Users[i]) + expectedUsers[i] = data.CreateUser(realm.Users[i], realm.Encoder) } } checkUsers(t, &expectedUsers, &users) @@ -465,10 +466,11 @@ func TestGetUsersSuccessfully(t *testing.T) { Name: sf.Format("realm_4_get_multiple_users_{0}", uuid.New().String()), TokenExpiration: 3600, RefreshTokenExpiration: 1800, + Encoder: encoding.NewPasswordJsonEncoder("salt"), } err := manager.CreateRealm(r) assert.NoError(t, err) - realm, err := manager.getRealmObject(r.Name) + realm, err := manager.GetRealm(r.Name) assert.NoError(t, err) // 2. Create multiple users users := make([]data.User, 3) @@ -480,7 +482,7 @@ func TestGetUsersSuccessfully(t *testing.T) { var rawUser interface{} err = json.Unmarshal([]byte(jsonStr), &rawUser) assert.NoError(t, err) - user := data.CreateUser(rawUser) + user := data.CreateUser(rawUser, r.Encoder) err = manager.CreateUser(realm.Name, user) users[i] = user assert.NoError(t, err) @@ -530,6 +532,7 @@ func TestGetUserByIdSuccessfully(t *testing.T) { Name: "realm_4_test_get_user_by_id", TokenExpiration: 3600, RefreshTokenExpiration: 1800, + Encoder: encoding.NewPasswordJsonEncoder("salt"), } err := manager.CreateRealm(realm) assert.NoError(t, err) @@ -540,7 +543,7 @@ func TestGetUserByIdSuccessfully(t *testing.T) { var rawUser interface{} err = json.Unmarshal([]byte(jsonStr), &rawUser) assert.NoError(t, err) - user := data.CreateUser(rawUser) + user := data.CreateUser(rawUser, realm.Encoder) err = manager.CreateUser(realm.Name, user) assert.NoError(t, err) @@ -603,7 +606,7 @@ func TestCreateUserSuccessfully(t *testing.T) { var rawUser interface{} err = json.Unmarshal([]byte(jsonStr), &rawUser) assert.NoError(t, err) - user := data.CreateUser(rawUser) + user := data.CreateUser(rawUser, r.Encoder) err = manager.CreateUser(realm.Name, user) assert.NoError(t, err) storedUser, err := manager.GetUser(realm.Name, tCase.userName) @@ -622,6 +625,7 @@ func TestCreateUserFailsDuplicateUser(t *testing.T) { Name: "realm_4_test_user_create_fails_duplicate", TokenExpiration: 3600, RefreshTokenExpiration: 1800, + Encoder: encoding.NewPasswordJsonEncoder("salt"), } err := manager.CreateRealm(realm) assert.NoError(t, err) @@ -631,7 +635,7 @@ func TestCreateUserFailsDuplicateUser(t *testing.T) { var rawUser interface{} err = json.Unmarshal([]byte(jsonStr), &rawUser) assert.NoError(t, err) - user := data.CreateUser(rawUser) + user := data.CreateUser(rawUser, realm.Encoder) err = manager.CreateUser(realm.Name, user) assert.NoError(t, err) @@ -650,6 +654,7 @@ func TestUpdateUserSuccessfully(t *testing.T) { Name: "realm_4_test_user_update", TokenExpiration: 3600, RefreshTokenExpiration: 1800, + Encoder: encoding.NewPasswordJsonEncoder("salt"), } err := manager.CreateRealm(realm) assert.NoError(t, err) @@ -660,7 +665,7 @@ func TestUpdateUserSuccessfully(t *testing.T) { var rawUser interface{} err = json.Unmarshal([]byte(jsonStr), &rawUser) assert.NoError(t, err) - user := data.CreateUser(rawUser) + user := data.CreateUser(rawUser, realm.Encoder) err = manager.CreateUser(realm.Name, user) assert.NoError(t, err) @@ -668,7 +673,7 @@ func TestUpdateUserSuccessfully(t *testing.T) { jsonStr = sf.Format(jsonTemplate, "pppetrov", "67890", "00000000-0000-0000-0000-000000000001") err = json.Unmarshal([]byte(jsonStr), &rawUser) assert.NoError(t, err) - user = data.CreateUser(rawUser) + user = data.CreateUser(rawUser, realm.Encoder) err = manager.UpdateUser(realm.Name, userName, user) assert.NoError(t, err) @@ -687,6 +692,7 @@ func TestUpdateUserFailsNonExistingUser(t *testing.T) { Name: "realm_4_test_user_update_fails_non_existing_user", TokenExpiration: 3600, RefreshTokenExpiration: 1800, + Encoder: encoding.NewPasswordJsonEncoder("salt"), } err := manager.CreateRealm(realm) assert.NoError(t, err) @@ -697,7 +703,7 @@ func TestUpdateUserFailsNonExistingUser(t *testing.T) { var rawUser interface{} err = json.Unmarshal([]byte(jsonStr), &rawUser) assert.NoError(t, err) - user := data.CreateUser(rawUser) + user := data.CreateUser(rawUser, realm.Encoder) err = manager.UpdateUser(realm.Name, userName, user) assert.Error(t, err) assert.True(t, errors.As(err, &appErrs.EmptyNotFoundErr)) @@ -713,6 +719,7 @@ func TestDeleteUserSuccessfully(t *testing.T) { Name: "realm_4_test_user_delete", TokenExpiration: 3600, RefreshTokenExpiration: 1800, + Encoder: encoding.NewPasswordJsonEncoder("salt"), } err := manager.CreateRealm(realm) assert.NoError(t, err) @@ -723,7 +730,7 @@ func TestDeleteUserSuccessfully(t *testing.T) { var rawUser interface{} err = json.Unmarshal([]byte(jsonStr), &rawUser) assert.NoError(t, err) - user := data.CreateUser(rawUser) + user := data.CreateUser(rawUser, realm.Encoder) err = manager.CreateUser(realm.Name, user) assert.NoError(t, err) u, err := manager.GetUser(realm.Name, userName) @@ -779,7 +786,7 @@ func TestGetUserFailsNonExistingUser(t *testing.T) { func TestChangeUserPasswordSuccessfully(t *testing.T) { manager := createTestRedisDataManager(t) // 1. Create Realm+Client+User - realm := data.Realm{ + realm := &data.Realm{ Name: sf.Format("app_4_user_pwd_change_check_{0}", uuid.New().String()), TokenExpiration: 3600, RefreshTokenExpiration: 1800, @@ -795,10 +802,10 @@ func TestChangeUserPasswordSuccessfully(t *testing.T) { }, } realm.Clients = append(realm.Clients, client) - err := manager.CreateRealm(realm) + err := manager.CreateRealm(*realm) assert.NoError(t, err) - createdRealm, err := manager.getRealmObject(realm.Name) + realm, err = manager.GetRealm(realm.Name) assert.NoError(t, err) userName := "new_app_user" @@ -807,7 +814,7 @@ func TestChangeUserPasswordSuccessfully(t *testing.T) { var rawUser interface{} err = json.Unmarshal([]byte(userJson), &rawUser) assert.NoError(t, err) - user := data.CreateUser(rawUser) + user := data.CreateUser(rawUser, realm.Encoder) err = manager.CreateUser(realm.Name, user) assert.NoError(t, err) @@ -816,11 +823,12 @@ func TestChangeUserPasswordSuccessfully(t *testing.T) { err = manager.SetPassword(realm.Name, userName, newPassword) assert.NoError(t, err) + var rawUser2 interface{} userJson = sf.Format(userTemplate, userName, newPassword) - err = json.Unmarshal([]byte(userJson), &rawUser) + err = json.Unmarshal([]byte(userJson), &rawUser2) + assert.NoError(t, err) + expectedUser := data.CreateUser(rawUser2, realm.Encoder) assert.NoError(t, err) - expectedUser := data.CreateUser(rawUser) - expectedUser.HashPassword(createdRealm.PasswordSalt) u, err := manager.GetUser(realm.Name, userName) assert.NoError(t, err) checkUser(t, &expectedUser, &u) diff --git a/managers/redis/manager_user_operations.go b/managers/redis/manager_user_operations.go index 46b7f80..de4339a 100644 --- a/managers/redis/manager_user_operations.go +++ b/managers/redis/manager_user_operations.go @@ -57,8 +57,12 @@ func (mn *RedisDataManager) GetUsers(realmName string) ([]data.User, error) { } userData := make([]data.User, len(realmUsersData)) + realm, err := mn.GetRealm(realmName) + if err != nil { + return []data.User{}, nil + } for i, u := range realmUsersData { - userData[i] = data.CreateUser(u) + userData[i] = data.CreateUser(u, realm.Encoder) } return userData, nil } @@ -83,7 +87,11 @@ func (mn *RedisDataManager) GetUser(realmName string, userName string) (data.Use } return nil, errors2.NewUnknownError("getSingleRedisObject", "RedisDataManager.GetUser", err) } - user := data.CreateUser(*rawUser) + realm, err := mn.GetRealm(realmName) + if err != nil { + return nil, err + } + user := data.CreateUser(*rawUser, realm.Encoder) return user, nil } @@ -129,7 +137,7 @@ func (mn *RedisDataManager) CreateUser(realmName string, userNew data.User) erro } // TODO(SIA) Add transaction // TODO(SIA) use function isExists - realm, err := mn.getRealmObject(realmName) + _, err := mn.GetRealm(realmName) if err != nil { mn.logger.Warn(sf.Format("CreateUser: GetRealmObject failed, error: {0}", err.Error())) return err @@ -144,7 +152,6 @@ func (mn *RedisDataManager) CreateUser(realmName string, userNew data.User) erro mn.logger.Warn(sf.Format("CreateUser: GetUser failed, error: {0}", err.Error())) return err } - userNew.HashPassword(realm.PasswordSalt) upsertUserErr := mn.upsertUserObject(realmName, userName, userNew.GetJsonString()) if upsertUserErr != nil { mn.logger.Error(sf.Format("CreateUser: addUserToRealm failed, error: {0}", upsertUserErr.Error())) @@ -242,11 +249,11 @@ func (mn *RedisDataManager) SetPassword(realmName string, userName string, passw if err != nil { return errors2.NewUnknownError("GetUser", "RedisDataManager.SetPassword", err) } - realm, err := mn.getRealmObject(realmName) + realm, err := mn.GetRealm(realmName) if err != nil { - return errors2.NewUnknownError("getRealmObject", "RedisDataManager.SetPassword", err) + return errors2.NewUnknownError("GetRealm", "RedisDataManager.SetPassword", err) } - if setPasswordErr := user.SetPassword(password, realm.PasswordSalt); setPasswordErr != nil { + if setPasswordErr := user.SetPassword(password, realm.Encoder); setPasswordErr != nil { return errors2.NewUnknownError("SetPassword", "RedisDataManager.SetPassword", setPasswordErr) } if upsertUserErr := mn.upsertUserObject(realmName, userName, user.GetJsonString()); upsertUserErr != nil { diff --git a/services/token_based_security.go b/services/token_based_security.go index 2517b00..a9bf1e1 100644 --- a/services/token_based_security.go +++ b/services/token_based_security.go @@ -11,7 +11,6 @@ import ( "github.com/wissance/Ferrum/errors" "github.com/wissance/Ferrum/logging" "github.com/wissance/Ferrum/managers" - b64hasher "github.com/wissance/Ferrum/utils/hasher" ) // TokenBasedSecurityService structure that implements SecurityService @@ -88,7 +87,7 @@ func (service *TokenBasedSecurityService) CheckCredentials(tokenIssueData *dto.T return &data.OperationError{Msg: "federated user not supported", Description: msg} } else { oldPasswordHash := user.GetPasswordHash() - if !b64hasher.IsPasswordsMatch(tokenIssueData.Password, realm.PasswordSalt, oldPasswordHash) { + if !realm.Encoder.IsPasswordsMatch(tokenIssueData.Password, oldPasswordHash) { service.logger.Trace("Credential check: password mismatch") return &data.OperationError{Msg: errors.InvalidUserCredentialsMsg, Description: errors.InvalidUserCredentialsDesc} } diff --git a/utils/hasher/b64hasher.go b/utils/encoding/encoding.go similarity index 54% rename from utils/hasher/b64hasher.go rename to utils/encoding/encoding.go index 5e2ff1a..a48e5d5 100644 --- a/utils/hasher/b64hasher.go +++ b/utils/encoding/encoding.go @@ -1,36 +1,52 @@ -package b64hasher +package encoding import ( "crypto/sha512" "encoding/base64" + "hash" "math/rand" ) -func GenerateSalt() string { - const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+=" - salt := make([]byte, 32) - for i := range salt { - salt[i] = charset[rand.Intn(len(charset))] - } - return string(salt) +type PasswordJsonEncoder struct { + salt string + hasher hash.Hash } -func HashPassword(password, salt string) string { - passwordBytes := []byte(password + salt) +func NewPasswordJsonEncoder(salt string) PasswordJsonEncoder { + encoder := PasswordJsonEncoder{ + hasher: sha512.New(), + salt: salt, + } + return encoder +} - sha512Hasher := sha512.New() - sha512Hasher.Write(passwordBytes) +func (e *PasswordJsonEncoder) HashPassword(password string) string { + if IsPasswordHashed(password) { + return password + } + passwordBytes := []byte(password + e.salt) + e.hasher.Write(passwordBytes) + hashedPasswordBytes := e.hasher.Sum(nil) + e.hasher.Reset() - hashedPasswordBytes := sha512Hasher.Sum(nil) b64encoded := b64Encode(hashedPasswordBytes) return b64encoded } -func IsPasswordsMatch(password, salt, hash string) bool { - currPasswordHash := HashPassword(password, salt) +func (e *PasswordJsonEncoder) IsPasswordsMatch(password, hash string) bool { + currPasswordHash := e.HashPassword(password) return b64Decode(hash) == b64Decode(currPasswordHash) } +func GenerateRandomSalt() string { + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+=" + salt := make([]byte, 32) + for i := range salt { + salt[i] = charset[rand.Intn(len(charset))] + } + return string(salt) +} + func IsPasswordHashed(password string) bool { decoded := b64Decode(password) if len(decoded) == 0 { diff --git a/utils/hasher/b64hasher_test.go b/utils/encoding/encoding_test.go similarity index 61% rename from utils/hasher/b64hasher_test.go rename to utils/encoding/encoding_test.go index c0d72f9..7bbfc2a 100644 --- a/utils/hasher/b64hasher_test.go +++ b/utils/encoding/encoding_test.go @@ -1,4 +1,4 @@ -package b64hasher +package encoding import ( "testing" @@ -11,10 +11,11 @@ func Test_HashPassword(t *testing.T) { // Arrange pwd := "qwerty" salt := "salt" + encoder := NewPasswordJsonEncoder(salt) // Act - hashedPwd := HashPassword(pwd, salt) - isMatch := IsPasswordsMatch(pwd, salt, hashedPwd) + hashedPwd := encoder.HashPassword(pwd) + isMatch := encoder.IsPasswordsMatch(pwd, hashedPwd) // Assert assert.True(t, isMatch) From 9ab845e4bba9dfa397b2e549d4831057d1960b72 Mon Sep 17 00:00:00 2001 From: Ushakov Michale Date: Fri, 10 Jan 2025 21:16:32 +0500 Subject: [PATCH 04/16] Method rename && CreateUser anti panic err omit --- api/routing_fuzzing_test.go | 2 +- application/application_test.go | 2 +- data/keycloak_user.go | 5 +++-- utils/encoding/encoding.go | 4 ++-- utils/encoding/encoding_test.go | 2 +- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/api/routing_fuzzing_test.go b/api/routing_fuzzing_test.go index 2e7c717..280e31c 100644 --- a/api/routing_fuzzing_test.go +++ b/api/routing_fuzzing_test.go @@ -32,7 +32,7 @@ const ( var ( testSalt = "salt" encoder = encoding.NewPasswordJsonEncoder(testSalt) - testHashedPassowrd = encoder.HashPassword("1234567890") + testHashedPassowrd = encoder.GetB64PasswordHash("1234567890") testKey = []byte("qwerty1234567890") testServerData = data.ServerData{ Realms: []data.Realm{ diff --git a/application/application_test.go b/application/application_test.go index 7138db1..f2f680d 100644 --- a/application/application_test.go +++ b/application/application_test.go @@ -32,7 +32,7 @@ const ( var ( testSalt = "salt" encoder = encoding.NewPasswordJsonEncoder(testSalt) - testHashedPassowrd = encoder.HashPassword("1234567890") + testHashedPassowrd = encoder.GetB64PasswordHash("1234567890") testKey = []byte("qwerty1234567890") testServerData = data.ServerData{ Realms: []data.Realm{ diff --git a/data/keycloak_user.go b/data/keycloak_user.go index d5c4409..a097bda 100644 --- a/data/keycloak_user.go +++ b/data/keycloak_user.go @@ -30,7 +30,8 @@ func CreateUser(rawData interface{}, encoder encoding.PasswordJsonEncoder) User jsonData, _ := json.Marshal(&rawData) kcUser := &KeyCloakUser{rawData: rawData, jsonRawData: string(jsonData)} password := getPathStringValue[string](kcUser.rawData, pathToPassword) - kcUser.SetPassword(password, encoder) + // todo(UMV): handle CreateUser errors in the future + _ = kcUser.SetPassword(password, encoder) user := User(kcUser) return user } @@ -66,7 +67,7 @@ func (user *KeyCloakUser) GetPasswordHash() string { * - encoder - encoder object with salt and hasher */ func (user *KeyCloakUser) SetPassword(password string, encoder encoding.PasswordJsonEncoder) error { - hashed := encoder.HashPassword(password) + hashed := encoder.GetB64PasswordHash(password) if err := setPathStringValue(user.rawData, pathToPassword, hashed); err != nil { return err } diff --git a/utils/encoding/encoding.go b/utils/encoding/encoding.go index a48e5d5..544c4c1 100644 --- a/utils/encoding/encoding.go +++ b/utils/encoding/encoding.go @@ -20,7 +20,7 @@ func NewPasswordJsonEncoder(salt string) PasswordJsonEncoder { return encoder } -func (e *PasswordJsonEncoder) HashPassword(password string) string { +func (e *PasswordJsonEncoder) GetB64PasswordHash(password string) string { if IsPasswordHashed(password) { return password } @@ -34,7 +34,7 @@ func (e *PasswordJsonEncoder) HashPassword(password string) string { } func (e *PasswordJsonEncoder) IsPasswordsMatch(password, hash string) bool { - currPasswordHash := e.HashPassword(password) + currPasswordHash := e.GetB64PasswordHash(password) return b64Decode(hash) == b64Decode(currPasswordHash) } diff --git a/utils/encoding/encoding_test.go b/utils/encoding/encoding_test.go index 7bbfc2a..8402c24 100644 --- a/utils/encoding/encoding_test.go +++ b/utils/encoding/encoding_test.go @@ -14,7 +14,7 @@ func Test_HashPassword(t *testing.T) { encoder := NewPasswordJsonEncoder(salt) // Act - hashedPwd := encoder.HashPassword(pwd) + hashedPwd := encoder.GetB64PasswordHash(pwd) isMatch := encoder.IsPasswordsMatch(pwd, hashedPwd) // Assert From a5086ac338cfab256a446f8003e8f9a3c5f664e9 Mon Sep 17 00:00:00 2001 From: Ushakov Michale Date: Sat, 11 Jan 2025 23:57:10 +0500 Subject: [PATCH 05/16] fixed user create|update --- api/admin/cli/main.go | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/api/admin/cli/main.go b/api/admin/cli/main.go index 410ec52..6ff127d 100644 --- a/api/admin/cli/main.go +++ b/api/admin/cli/main.go @@ -122,10 +122,7 @@ func main() { if err := json.Unmarshal(value, &userNew); err != nil { log.Fatalf("json.Unmarshal failed: %s", err) } - if resourceId == "" { - log.Fatalf("Realm name not specified") - } - realm, err := manager.GetRealm(resourceId) + realm, err := manager.GetRealm(params) if err != nil { log.Fatalf("GetRealm failed: %s", err) } @@ -210,10 +207,7 @@ func main() { if err := json.Unmarshal(value, &newUser); err != nil { log.Fatalf("json.Unmarshal failed: %s", err) } - if resourceId == "" { - log.Fatalf("Realm name not specified") - } - realm, err := manager.GetRealm(resourceId) + realm, err := manager.GetRealm(params) if err != nil { log.Fatalf("GetRealm failed: %s", err) } From b6c83a36078cbb31187749802f400c4bc8001f0a Mon Sep 17 00:00:00 2001 From: Ushakov Michale Date: Sun, 12 Jan 2025 00:04:07 +0500 Subject: [PATCH 06/16] more detailed args --- api/admin/cli/main.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/api/admin/cli/main.go b/api/admin/cli/main.go index 6ff127d..2fced84 100644 --- a/api/admin/cli/main.go +++ b/api/admin/cli/main.go @@ -20,12 +20,12 @@ import ( const defaultConfig = "./config_w_redis.json" var ( - argConfigFile = flag.String("config", defaultConfig, "") - argOperation = flag.String("operation", "", "") - argResource = flag.String("resource", "", "") - argResourceId = flag.String("resource_id", "", "") - argParams = flag.String("params", "", "This is the name of the realm for operations on client or user resources") - argValue = flag.String("value", "", "Json object") + argConfigFile = flag.String("config", defaultConfig, "Application config for working with a persistent data store") + argOperation = flag.String("operation", "", "One of the available operations read|create|update|delete or user specific change/reset password") + argResource = flag.String("resource", "", "\"realm\", \"client\" or \"user\" or maybe other in future") + argResourceId = flag.String("resource_id", "", "resource object identifier, id required for the update|delete or read operation") + argParams = flag.String("params", "", "Name of a realm for operations on client or user resources") + argValue = flag.String("value", "", "Json encoded resource itself") ) func main() { From 53c4913bc7a2957bc971768c499462ce604baee0 Mon Sep 17 00:00:00 2001 From: Ushakov Michale Date: Sun, 12 Jan 2025 20:02:47 +0500 Subject: [PATCH 07/16] added dada for additional Realm --- tools/create_wissance_demo_users_docker.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tools/create_wissance_demo_users_docker.sh b/tools/create_wissance_demo_users_docker.sh index ccb1db5..fd7ed59 100644 --- a/tools/create_wissance_demo_users_docker.sh +++ b/tools/create_wissance_demo_users_docker.sh @@ -1,3 +1,8 @@ +# Realm WissanceFerrumDemo ./ferrum-admin --config=config_docker_w_redis.json --resource=realm --operation=create --value='{"name": "WissanceFerrumDemo", "user_federation_services":[], "token_expiration": 600, "refresh_expiration": 300}' ./ferrum-admin --config=config_docker_w_redis.json --resource=client --operation=create --value='{"id": "d4dc483d-7d0d-4d2e-a0a0-2d34b55e6667", "name": "WissanceWebDemo", "type": "confidential", "auth": {"type": 1, "value": "fb6Z4RsOadVycQoeQiN57xpu8w8w1111"}}' --params="WissanceFerrumDemo" ./ferrum-admin --config=config_docker_w_redis.json --resource=user --operation=create --value='{"info": {"sub": "667ff6a7-3f6b-449b-a217-6fc5d9ac6891", "email_verified": true, "roles": ["admin"], "name": "M.V.Ushakov", "preferred_username": "umv", "given_name": "Michael", "family_name": "Ushakov"}, "credentials": {"password": "1s2d3f4g90xs"}}' --params="WissanceFerrumDemo" +# Realm WissanceFerrumDemo2 +./ferrum-admin --config=config_docker_w_redis.json --resource=realm --operation=create --value='{"name": "WissanceFerrumDemo2", "user_federation_services":[], "token_expiration": 600, "refresh_expiration": 300}' +./ferrum-admin --config=config_docker_w_redis.json --resource=client --operation=create --value='{"id": "d4dc483d-7d0d-4d2e-a0a0-2d34b55e6668", "name": "WissanceWebDemo2", "type": "confidential", "auth": {"type": 1, "value": "fb6Z4RsOadVycQoeQiN57xpu8w8w2222"}}' --params="WissanceFerrumDemo2" +./ferrum-admin --config=config_docker_w_redis.json --resource=user --operation=create --value='{"info": {"sub": "667ff6a7-3f6b-449b-a217-6fc5d9ac6892", "email_verified": true, "roles": ["manager"], "name": "A.Petrov", "preferred_username": "paa", "given_name": "Alex", "family_name": "Petrov"}, "credentials": {"password": "12345678"}}' --params="WissanceFerrumDemo2" From f7c10945de207df3b031ae8008fdad49868e1422 Mon Sep 17 00:00:00 2001 From: Ushakov Michale Date: Sun, 12 Jan 2025 20:24:53 +0500 Subject: [PATCH 08/16] clarification --- api/admin/cli/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/admin/cli/main.go b/api/admin/cli/main.go index 2fced84..9726551 100644 --- a/api/admin/cli/main.go +++ b/api/admin/cli/main.go @@ -251,7 +251,7 @@ func main() { } // TODO(SIA) Moving password verification to another location if len(value) < 8 { - log.Fatalf("Password length must be greater than 8") + log.Fatalf("Password length must be greater than 7") } password := string(value) passwordManager := manager.(PasswordManager) From c9c6765520aa668a72b71df9f520c0711b9573a1 Mon Sep 17 00:00:00 2001 From: Yuri Shangaraev Date: Mon, 13 Jan 2025 14:38:19 +0500 Subject: [PATCH 09/16] add nil check for encoder in data.CreateUser this is necessary to avoid multiple hashing of passwords --- .gitignore | 2 ++ api/admin/cli/main.go | 6 +----- data/keycloak_user.go | 13 ++++++------- data/realm.go | 2 +- data/user.go | 2 +- managers/files/manager.go | 3 +-- managers/files/manager_test.go | 8 ++------ managers/redis/manager_realm_operations.go | 2 +- managers/redis/manager_test.go | 14 +++++--------- managers/redis/manager_user_operations.go | 12 ++---------- utils/encoding/encoding.go | 15 ++------------- 11 files changed, 24 insertions(+), 55 deletions(-) diff --git a/.gitignore b/.gitignore index f3d7fbd..15c0050 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,5 @@ extTestApps/Wissance.Auth.FerrumChecker/.vs/ extTestApps/Wissance.Auth.FerrumChecker/Wissance.Auth.FerrumChecker/bin/ extTestApps/Wissance.Auth.FerrumChecker/Wissance.Auth.FerrumChecker/obj/ + +.vscode/ \ No newline at end of file diff --git a/api/admin/cli/main.go b/api/admin/cli/main.go index 2fced84..188b52e 100644 --- a/api/admin/cli/main.go +++ b/api/admin/cli/main.go @@ -207,11 +207,7 @@ func main() { if err := json.Unmarshal(value, &newUser); err != nil { log.Fatalf("json.Unmarshal failed: %s", err) } - realm, err := manager.GetRealm(params) - if err != nil { - log.Fatalf("GetRealm failed: %s", err) - } - user := data.CreateUser(newUser, realm.Encoder) + user := data.CreateUser(newUser, nil) if err := manager.UpdateUser(params, resourceId, user); err != nil { log.Fatalf("UpdateUser failed: %s", err) } diff --git a/data/keycloak_user.go b/data/keycloak_user.go index a097bda..78d3935 100644 --- a/data/keycloak_user.go +++ b/data/keycloak_user.go @@ -26,12 +26,14 @@ type KeyCloakUser struct { * - rawData - any json * Return: instance of User as KeyCloakUser */ -func CreateUser(rawData interface{}, encoder encoding.PasswordJsonEncoder) User { +func CreateUser(rawData interface{}, encoder *encoding.PasswordJsonEncoder) User { jsonData, _ := json.Marshal(&rawData) kcUser := &KeyCloakUser{rawData: rawData, jsonRawData: string(jsonData)} password := getPathStringValue[string](kcUser.rawData, pathToPassword) - // todo(UMV): handle CreateUser errors in the future - _ = kcUser.SetPassword(password, encoder) + if encoder != nil { + // todo(UMV): handle CreateUser errors in the future + _ = kcUser.SetPassword(password, encoder) + } user := User(kcUser) return user } @@ -54,9 +56,6 @@ func (user *KeyCloakUser) GetUsername() string { // todo(UMV): we should consider case when User is External func (user *KeyCloakUser) GetPasswordHash() string { password := getPathStringValue[string](user.rawData, pathToPassword) - if !encoding.IsPasswordHashed(password) { - // todo (YuriShang): think about actions if the password is not hashed - } return password } @@ -66,7 +65,7 @@ func (user *KeyCloakUser) GetPasswordHash() string { * - password - new password * - encoder - encoder object with salt and hasher */ -func (user *KeyCloakUser) SetPassword(password string, encoder encoding.PasswordJsonEncoder) error { +func (user *KeyCloakUser) SetPassword(password string, encoder *encoding.PasswordJsonEncoder) error { hashed := encoder.GetB64PasswordHash(password) if err := setPathStringValue(user.rawData, pathToPassword, hashed); err != nil { return err diff --git a/data/realm.go b/data/realm.go index 3317127..cea2012 100644 --- a/data/realm.go +++ b/data/realm.go @@ -15,5 +15,5 @@ type Realm struct { RefreshTokenExpiration int `json:"refresh_expiration"` UserFederationServices []UserFederationServiceConfig `json:"user_federation_services"` PasswordSalt string `json:"password_salt"` - Encoder encoding.PasswordJsonEncoder + Encoder *encoding.PasswordJsonEncoder } diff --git a/data/user.go b/data/user.go index 7b49f2d..26ba1ca 100644 --- a/data/user.go +++ b/data/user.go @@ -10,7 +10,7 @@ import ( type User interface { GetUsername() string GetPasswordHash() string - SetPassword(password string, encoder encoding.PasswordJsonEncoder) error + SetPassword(password string, encoder *encoding.PasswordJsonEncoder) error GetId() uuid.UUID GetUserInfo() interface{} GetRawData() interface{} diff --git a/managers/files/manager.go b/managers/files/manager.go index c38a753..3999d50 100644 --- a/managers/files/manager.go +++ b/managers/files/manager.go @@ -103,9 +103,8 @@ func (mn *FileDataManager) GetUsers(realmName string) ([]data.User, error) { return nil, errors.ErrZeroLength } users := make([]data.User, len(e.Users)) - e.Encoder = encoding.NewPasswordJsonEncoder(e.PasswordSalt) for i, u := range e.Users { - user := data.CreateUser(u, e.Encoder) + user := data.CreateUser(u, nil) users[i] = user } return users, nil diff --git a/managers/files/manager_test.go b/managers/files/manager_test.go index c0d7806..ebb077f 100644 --- a/managers/files/manager_test.go +++ b/managers/files/manager_test.go @@ -68,9 +68,7 @@ func TestGetUserSuccessfully(t *testing.T) { var rawUser interface{} err := json.Unmarshal([]byte(userJson), &rawUser) assert.NoError(t, err) - r, err := manager.GetRealm(realm) - assert.NoError(t, err) - expectedUser := data.CreateUser(rawUser, r.Encoder) + expectedUser := data.CreateUser(rawUser, nil) user, err := manager.GetUser(realm, userName) assert.NoError(t, err) checkUser(t, &expectedUser, &user) @@ -99,9 +97,7 @@ func TestGetUserByIdSuccessfully(t *testing.T) { var rawUser interface{} err := json.Unmarshal([]byte(userJson), &rawUser) assert.NoError(t, err) - r, err := manager.GetRealm(realm) - assert.NoError(t, err) - expectedUser := data.CreateUser(rawUser, r.Encoder) + expectedUser := data.CreateUser(rawUser, nil) user, err := manager.GetUserById(realm, userId) assert.NoError(t, err) checkUser(t, &expectedUser, &user) diff --git a/managers/redis/manager_realm_operations.go b/managers/redis/manager_realm_operations.go index e5f3f1b..86773ef 100755 --- a/managers/redis/manager_realm_operations.go +++ b/managers/redis/manager_realm_operations.go @@ -129,7 +129,7 @@ func (mn *RedisDataManager) CreateRealm(newRealm data.Realm) error { TokenExpiration: newRealm.TokenExpiration, RefreshTokenExpiration: newRealm.RefreshTokenExpiration, PasswordSalt: salt, - Encoder: encoding.PasswordJsonEncoder{}, + Encoder: nil, } jsonShortRealm, err := json.Marshal(shortRealm) if err != nil { diff --git a/managers/redis/manager_test.go b/managers/redis/manager_test.go index 51722a1..838a79f 100644 --- a/managers/redis/manager_test.go +++ b/managers/redis/manager_test.go @@ -466,7 +466,6 @@ func TestGetUsersSuccessfully(t *testing.T) { Name: sf.Format("realm_4_get_multiple_users_{0}", uuid.New().String()), TokenExpiration: 3600, RefreshTokenExpiration: 1800, - Encoder: encoding.NewPasswordJsonEncoder("salt"), } err := manager.CreateRealm(r) assert.NoError(t, err) @@ -482,7 +481,7 @@ func TestGetUsersSuccessfully(t *testing.T) { var rawUser interface{} err = json.Unmarshal([]byte(jsonStr), &rawUser) assert.NoError(t, err) - user := data.CreateUser(rawUser, r.Encoder) + user := data.CreateUser(rawUser, nil) err = manager.CreateUser(realm.Name, user) users[i] = user assert.NoError(t, err) @@ -532,7 +531,6 @@ func TestGetUserByIdSuccessfully(t *testing.T) { Name: "realm_4_test_get_user_by_id", TokenExpiration: 3600, RefreshTokenExpiration: 1800, - Encoder: encoding.NewPasswordJsonEncoder("salt"), } err := manager.CreateRealm(realm) assert.NoError(t, err) @@ -543,7 +541,7 @@ func TestGetUserByIdSuccessfully(t *testing.T) { var rawUser interface{} err = json.Unmarshal([]byte(jsonStr), &rawUser) assert.NoError(t, err) - user := data.CreateUser(rawUser, realm.Encoder) + user := data.CreateUser(rawUser, nil) err = manager.CreateUser(realm.Name, user) assert.NoError(t, err) @@ -673,7 +671,7 @@ func TestUpdateUserSuccessfully(t *testing.T) { jsonStr = sf.Format(jsonTemplate, "pppetrov", "67890", "00000000-0000-0000-0000-000000000001") err = json.Unmarshal([]byte(jsonStr), &rawUser) assert.NoError(t, err) - user = data.CreateUser(rawUser, realm.Encoder) + user = data.CreateUser(rawUser, nil) err = manager.UpdateUser(realm.Name, userName, user) assert.NoError(t, err) @@ -692,7 +690,6 @@ func TestUpdateUserFailsNonExistingUser(t *testing.T) { Name: "realm_4_test_user_update_fails_non_existing_user", TokenExpiration: 3600, RefreshTokenExpiration: 1800, - Encoder: encoding.NewPasswordJsonEncoder("salt"), } err := manager.CreateRealm(realm) assert.NoError(t, err) @@ -703,7 +700,7 @@ func TestUpdateUserFailsNonExistingUser(t *testing.T) { var rawUser interface{} err = json.Unmarshal([]byte(jsonStr), &rawUser) assert.NoError(t, err) - user := data.CreateUser(rawUser, realm.Encoder) + user := data.CreateUser(rawUser, nil) err = manager.UpdateUser(realm.Name, userName, user) assert.Error(t, err) assert.True(t, errors.As(err, &appErrs.EmptyNotFoundErr)) @@ -719,7 +716,6 @@ func TestDeleteUserSuccessfully(t *testing.T) { Name: "realm_4_test_user_delete", TokenExpiration: 3600, RefreshTokenExpiration: 1800, - Encoder: encoding.NewPasswordJsonEncoder("salt"), } err := manager.CreateRealm(realm) assert.NoError(t, err) @@ -730,7 +726,7 @@ func TestDeleteUserSuccessfully(t *testing.T) { var rawUser interface{} err = json.Unmarshal([]byte(jsonStr), &rawUser) assert.NoError(t, err) - user := data.CreateUser(rawUser, realm.Encoder) + user := data.CreateUser(rawUser, nil) err = manager.CreateUser(realm.Name, user) assert.NoError(t, err) u, err := manager.GetUser(realm.Name, userName) diff --git a/managers/redis/manager_user_operations.go b/managers/redis/manager_user_operations.go index de4339a..ac4dbbd 100644 --- a/managers/redis/manager_user_operations.go +++ b/managers/redis/manager_user_operations.go @@ -57,12 +57,8 @@ func (mn *RedisDataManager) GetUsers(realmName string) ([]data.User, error) { } userData := make([]data.User, len(realmUsersData)) - realm, err := mn.GetRealm(realmName) - if err != nil { - return []data.User{}, nil - } for i, u := range realmUsersData { - userData[i] = data.CreateUser(u, realm.Encoder) + userData[i] = data.CreateUser(u, nil) } return userData, nil } @@ -87,11 +83,7 @@ func (mn *RedisDataManager) GetUser(realmName string, userName string) (data.Use } return nil, errors2.NewUnknownError("getSingleRedisObject", "RedisDataManager.GetUser", err) } - realm, err := mn.GetRealm(realmName) - if err != nil { - return nil, err - } - user := data.CreateUser(*rawUser, realm.Encoder) + user := data.CreateUser(*rawUser, nil) return user, nil } diff --git a/utils/encoding/encoding.go b/utils/encoding/encoding.go index 544c4c1..d513de0 100644 --- a/utils/encoding/encoding.go +++ b/utils/encoding/encoding.go @@ -12,18 +12,15 @@ type PasswordJsonEncoder struct { hasher hash.Hash } -func NewPasswordJsonEncoder(salt string) PasswordJsonEncoder { +func NewPasswordJsonEncoder(salt string) *PasswordJsonEncoder { encoder := PasswordJsonEncoder{ hasher: sha512.New(), salt: salt, } - return encoder + return &encoder } func (e *PasswordJsonEncoder) GetB64PasswordHash(password string) string { - if IsPasswordHashed(password) { - return password - } passwordBytes := []byte(password + e.salt) e.hasher.Write(passwordBytes) hashedPasswordBytes := e.hasher.Sum(nil) @@ -47,14 +44,6 @@ func GenerateRandomSalt() string { return string(salt) } -func IsPasswordHashed(password string) bool { - decoded := b64Decode(password) - if len(decoded) == 0 { - return false - } - return true -} - func b64Encode(encoded []byte) string { cstr := base64.URLEncoding.EncodeToString(encoded) return cstr From 7d1c29b6df3bfdc396b38d32d54b541e5d09b274 Mon Sep 17 00:00:00 2001 From: Ushakov Michale Date: Thu, 16 Jan 2025 18:15:12 +0500 Subject: [PATCH 10/16] File was configured to be working with hashes insead of raw passwords --- application/application_test.go | 4 ++-- data.json | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/application/application_test.go b/application/application_test.go index f2f680d..6a02e82 100644 --- a/application/application_test.go +++ b/application/application_test.go @@ -32,7 +32,7 @@ const ( var ( testSalt = "salt" encoder = encoding.NewPasswordJsonEncoder(testSalt) - testHashedPassowrd = encoder.GetB64PasswordHash("1234567890") + testHashedPassword = encoder.GetB64PasswordHash("1234567890") testKey = []byte("qwerty1234567890") testServerData = data.ServerData{ Realms: []data.Realm{ @@ -51,7 +51,7 @@ var ( "name": "vano", "preferred_username": "vano", "given_name": "vano ivanov", "family_name": "ivanov", "email_verified": true, }, - "credentials": map[string]interface{}{"password": testHashedPassowrd}, + "credentials": map[string]interface{}{"password": testHashedPassword}, }, }, PasswordSalt: testSalt, diff --git a/data.json b/data.json index eb262a6..0d9c4dc 100644 --- a/data.json +++ b/data.json @@ -4,6 +4,7 @@ "name": "myapp", "token_expiration": 330, "refresh_expiration": 200, + "password_salt": "1234567890", "clients": [ { "id": "d4dc483d-7d0d-4d2e-a0a0-2d34b55e5a14", @@ -29,7 +30,7 @@ "family_name": "sys" }, "credentials": { - "password": "1s2d3f4g90xs" + "password": "AcMWCBu5AQDN8IvSRExUUSQq7H3RH6IzsxZJqyIoEmPFtJwGknUUvzet0vhS95hgkrKLNM66v0mUB5xji8zdqA==" } }, { From cdee5034bbb4d4c044b123d102fb2445f5bf84ac Mon Sep 17 00:00:00 2001 From: Ushakov Michale Date: Thu, 16 Jan 2025 18:50:23 +0500 Subject: [PATCH 11/16] added changes in readme.md --- README.md | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 65 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a583314..34286f9 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Ferrum is a **better** Authorization Server, this is a Community version. ![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/wissance/Ferrum?style=plastic) ![GitHub issues](https://img.shields.io/github/issues/wissance/Ferrum?style=plastic) ![GitHub Release Date](https://img.shields.io/github/release-date/wissance/Ferrum) -![GitHub release (latest by date)](https://img.shields.io/github/downloads/wissance/Ferrum/v0.9.1/total?style=plastic) +![GitHub release (latest by date)](https://img.shields.io/github/downloads/wissance/Ferrum/v0.9.2/total?style=plastic) ![Ferrum: A better Auth Server](/img/ferrum_cover.png) @@ -43,6 +43,8 @@ it has `endpoints` SIMILAR to `Keycloak`, at present time we are having followin ## 3. How to use +`Ferrum` is thoroughly developing with maximal quality of code and solution; we are working using a `git-flow` approach; even `master` branch is a stable release branch, but `develop` is also highly stable, therefore develop version could also be used in a production. + ### 3.1 Build First of all build is simple run `go build` from application root directory. Additionally it is possible @@ -193,7 +195,68 @@ Since version `0.9.1` it is possible to use `CLI Admin` [See](api/admin/cli/READ ![Use CLI Admin from docker](/img/additional/cli_from_docker.png) -## 6. Contributors +## 6. Changes + +Brief info about changes in releases. + +### 6.1 Changes in 0.0.1 + +Features: +* `Keycloak` compatible HTTP-endpoints to issue a new `token` and to get `userinfo` + +### 6.2 Changes in 0.1.0 + +Features: +* documentation (`readme.md` file) +* integration tests + +### 6.3 Changes in 0.1.1 + +Features: +* fixed modules names + +### 6.4 Changes in 0.1.2 + +Features: +* changed module names to make it available to embed `Ferrum` in an other applications + +### 6.5 Changes in 0.1.3 + +Features: +* `Keycloak` compatible HTTP-endpoint for token introspect + +### 6.6 Changes in 0.1.4 + +Features: +* removed `/` therefore it is possible to interact with `Ferrum` using `go-cloak` package + +### 6.7 Changes in 0.9.0 + +Features +* logging +* implemented token refresh +* better docs + +### 6.8 Changes in 0.9.1 + +Features: +* `docker` && `docker-compose` for app running +* admin `CLI` `API` +* `Redis` as a production data storage + +### 6.9 Changes in 0.9.2 + +Features: +* admin cli added to docker +* test on `Redis` data manger +* used different config to run locally and in docker +* newer `Keycloak` versions support +* checked stability if `Redis` is down, `Ferrum` does not crushes and wait until `Redis` is ready +* swagger (-devmode) and `Keycloak` compatible HTTP endpoint `openid-configuration` +* support for federated user (without full providers impl, just preliminary) +* store password as a hashes + +## 7. Contributors From 8e869bd90d62e2b50b0f4601fa9a78f5c73bfb4b Mon Sep 17 00:00:00 2001 From: Ushakov Michale Date: Thu, 16 Jan 2025 18:51:11 +0500 Subject: [PATCH 12/16] some clarifications --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 34286f9..9eb5a72 100644 --- a/README.md +++ b/README.md @@ -252,7 +252,7 @@ Features: * used different config to run locally and in docker * newer `Keycloak` versions support * checked stability if `Redis` is down, `Ferrum` does not crushes and wait until `Redis` is ready -* swagger (-devmode) and `Keycloak` compatible HTTP endpoint `openid-configuration` +* `swagger` (`-devmode` option in cmd line) and `Keycloak` compatible HTTP endpoint `openid-configuration` * support for federated user (without full providers impl, just preliminary) * store password as a hashes From 60d87820864ee58dbc7868a44e0a9a91424c24da Mon Sep 17 00:00:00 2001 From: Yuri Shangaraev Date: Fri, 17 Jan 2025 11:04:19 +0500 Subject: [PATCH 13/16] workflow -> workflows --- .github/{workflow => workflows}/ci.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/{workflow => workflows}/ci.yml (100%) diff --git a/.github/workflow/ci.yml b/.github/workflows/ci.yml similarity index 100% rename from .github/workflow/ci.yml rename to .github/workflows/ci.yml From 3a52f86de4fb91c86c573d1e1274c22f78619d44 Mon Sep 17 00:00:00 2001 From: Yuri Shangaraev Date: Fri, 17 Jan 2025 11:07:59 +0500 Subject: [PATCH 14/16] testing --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9eb5a72..bed7018 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Ferrum +# Ferrum . Ferrum is a **better** Authorization Server, this is a Community version. From 3f782ac5ae104c2154e5cbae0d054167c8919488 Mon Sep 17 00:00:00 2001 From: Yuri <95570582+YuriShang@users.noreply.github.com> Date: Fri, 17 Jan 2025 11:13:21 +0500 Subject: [PATCH 15/16] Update ci.yml --- .github/workflows/ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index abc620f..1f400fd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,8 +3,9 @@ name: Go CI on: push: branches: - - 'main' - - 'develop' + - main + - develop + - master jobs: build-linux: From b7db01dd71768410fb6fc428ebeef0d6d461d467 Mon Sep 17 00:00:00 2001 From: Yuri Shangaraev Date: Sat, 25 Jan 2025 19:08:42 +0500 Subject: [PATCH 16/16] fix ci, fix linter's remarks --- .github/workflows/ci.yml | 32 ++++---- .golangci.yaml | 75 +++++++++++++++++++ README.md | 69 +---------------- api/admin/cli/main.go | 4 +- api/admin/cli/operations/operations.go | 16 ++-- api/rest/web_api_handler.go | 6 +- api/routing_fuzzing_test.go | 31 ++++---- application/application.go | 58 +++++++++----- application/application_runner.go | 4 +- application/application_test.go | 4 +- config/logs_config.go | 9 +-- data/client.go | 2 +- data/keycloak_user.go | 4 +- data/keycloak_user_test.go | 1 - go.mod | 4 +- go.sum | 10 ++- main.go | 11 ++- managers/files/manager.go | 12 +-- managers/files/manager_test.go | 2 + managers/redis/manager.go | 15 ++-- managers/redis/manager_client_operations.go | 8 +- managers/redis/manager_test.go | 13 +++- ...ager_user_federation_service_operations.go | 5 +- managers/redis/manager_user_operations.go | 13 ++-- .../federation/ldap_federation_service.go | 6 +- 25 files changed, 235 insertions(+), 179 deletions(-) create mode 100644 .golangci.yaml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d5c453b..195ef37 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,9 @@ name: Go CI on: pull_request: - branches: [develop] + branches: [develop, master] + push: + branches: [master] jobs: build-linux: @@ -11,12 +13,11 @@ jobs: steps: - uses: actions/checkout@v4.1.7 - name: Set up Go - uses: actions/setup-go@v5.0.2 + uses: actions/setup-go@v5.2.0 with: - go-version: '1.19' - cache: false + go-version: '1.21' - name: Build - run: go mod tidy && go build -v ./... + run: go version && go build -v ./... build-windows: name: Build Ferrum on windows @@ -24,12 +25,11 @@ jobs: steps: - uses: actions/checkout@v4.1.7 - name: Set up Go - uses: actions/setup-go@v5.0.2 + uses: actions/setup-go@v5.2.0 with: - go-version: '1.19' - cache: false + go-version: '1.21' - name: Build - run: go mod tidy && go build -v ./... + run: go version && go build -v ./... all-tests-linux: name: Run all tests on linux @@ -37,16 +37,15 @@ jobs: steps: - uses: actions/checkout@v4.1.7 - name: Set up Go - uses: actions/setup-go@v5.0.2 + uses: actions/setup-go@v5.2.0 with: - go-version: '1.19' - cache: false + go-version: '1.21' - name: Set up Redis Stack server run: docker compose up -d redis - name: Get Redis logs run: docker logs $(docker ps -aqf "name=wissance_ferrum_db") - name: Test all - run: go test -v ./... + run: go version && go mod tidy && go test -v ./... #all-tests-windows: # name: Run all tests on windows @@ -69,12 +68,11 @@ jobs: steps: - uses: actions/checkout@v4.1.7 - name: Set up Go - uses: actions/setup-go@v5.0.2 + uses: actions/setup-go@v5.2.0 with: - go-version: '1.18' - cache: false + go-version: '1.21' - name: Run golangci-lint uses: golangci/golangci-lint-action@v6.1.0 with: version: v1.50.1 - args: --timeout 3m --config .golangci.yaml + args: --timeout 3m --config .golangci.yaml \ No newline at end of file diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..f980fc3 --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,75 @@ +run: + # timeout for analysis, e.g. 30s, 5m, default is 1m + timeout: 30m + + modules-download-mode: readonly + + go: '1.22' + +output: + # colored-line-number|line-number|json|tab|checkstyle|code-climate|junit-xml|github-actions + # default is "colored-line-number" + formats: code-climate + +linters: + enable-all: false + disable: + - exhaustruct + - gofumpt + - testpackage + - depguard + - tagliatelle + - ireturn + - varnamelen + - wrapcheck + +linters-settings: + stylecheck: + # Select the Go version to target. The default is '1.13'. + # https://staticcheck.io/docs/options#checks + checks: [ "all", "-ST1000" ] + funlen: + lines: 100 + gci: + sections: + - standard + - default + - prefix(gitlab.sima-land.ru/sl/it/dev-dep/web-services/ad-data-operator) + gocyclo: + min-complexity: 5 + varnamelen: + ignore-names: + - id + ignore-decls: + - ok bool + wrapcheck: + ignorePackageGlobs: + - google.golang.org/grpc/status + - github.com/pkg/errors + - golang.org/x/sync/errgroup + gosec: + excludes: + - G204 + +issues: + exclude-rules: + - path: _test\.go + linters: + - containedctx + - gocyclo + - cyclop + - funlen + - goerr113 + - varnamelen + - staticcheck + - maintidx + - lll + - paralleltest + - dupl + - typecheck + - wsl + - path: main\.go + linters: + - gochecknoglobals + - lll + - funlen diff --git a/README.md b/README.md index bed7018..a583314 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Ferrum . +# Ferrum Ferrum is a **better** Authorization Server, this is a Community version. @@ -6,7 +6,7 @@ Ferrum is a **better** Authorization Server, this is a Community version. ![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/wissance/Ferrum?style=plastic) ![GitHub issues](https://img.shields.io/github/issues/wissance/Ferrum?style=plastic) ![GitHub Release Date](https://img.shields.io/github/release-date/wissance/Ferrum) -![GitHub release (latest by date)](https://img.shields.io/github/downloads/wissance/Ferrum/v0.9.2/total?style=plastic) +![GitHub release (latest by date)](https://img.shields.io/github/downloads/wissance/Ferrum/v0.9.1/total?style=plastic) ![Ferrum: A better Auth Server](/img/ferrum_cover.png) @@ -43,8 +43,6 @@ it has `endpoints` SIMILAR to `Keycloak`, at present time we are having followin ## 3. How to use -`Ferrum` is thoroughly developing with maximal quality of code and solution; we are working using a `git-flow` approach; even `master` branch is a stable release branch, but `develop` is also highly stable, therefore develop version could also be used in a production. - ### 3.1 Build First of all build is simple run `go build` from application root directory. Additionally it is possible @@ -195,68 +193,7 @@ Since version `0.9.1` it is possible to use `CLI Admin` [See](api/admin/cli/READ ![Use CLI Admin from docker](/img/additional/cli_from_docker.png) -## 6. Changes - -Brief info about changes in releases. - -### 6.1 Changes in 0.0.1 - -Features: -* `Keycloak` compatible HTTP-endpoints to issue a new `token` and to get `userinfo` - -### 6.2 Changes in 0.1.0 - -Features: -* documentation (`readme.md` file) -* integration tests - -### 6.3 Changes in 0.1.1 - -Features: -* fixed modules names - -### 6.4 Changes in 0.1.2 - -Features: -* changed module names to make it available to embed `Ferrum` in an other applications - -### 6.5 Changes in 0.1.3 - -Features: -* `Keycloak` compatible HTTP-endpoint for token introspect - -### 6.6 Changes in 0.1.4 - -Features: -* removed `/` therefore it is possible to interact with `Ferrum` using `go-cloak` package - -### 6.7 Changes in 0.9.0 - -Features -* logging -* implemented token refresh -* better docs - -### 6.8 Changes in 0.9.1 - -Features: -* `docker` && `docker-compose` for app running -* admin `CLI` `API` -* `Redis` as a production data storage - -### 6.9 Changes in 0.9.2 - -Features: -* admin cli added to docker -* test on `Redis` data manger -* used different config to run locally and in docker -* newer `Keycloak` versions support -* checked stability if `Redis` is down, `Ferrum` does not crushes and wait until `Redis` is ready -* `swagger` (`-devmode` option in cmd line) and `Keycloak` compatible HTTP endpoint `openid-configuration` -* support for federated user (without full providers impl, just preliminary) -* store password as a hashes - -## 7. Contributors +## 6. Contributors diff --git a/api/admin/cli/main.go b/api/admin/cli/main.go index 87b4995..85ec819 100644 --- a/api/admin/cli/main.go +++ b/api/admin/cli/main.go @@ -110,10 +110,10 @@ func main() { case operations.ClientResource: var clientNew data.Client if unmarshalErr := json.Unmarshal(value, &clientNew); unmarshalErr != nil { - log.Fatalf(sf.Format("json.Unmarshal failed: {0}", unmarshalErr.Error())) + log.Fatal(sf.Format("json.Unmarshal failed: {0}", unmarshalErr.Error())) } if createErr := manager.CreateClient(params, clientNew); createErr != nil { - log.Fatalf(sf.Format("CreateClient failed: {0}", createErr.Error())) + log.Fatal(sf.Format("CreateClient failed: {0}", createErr.Error())) } log.Print(sf.Format("Client: \"{0}\" successfully created", clientNew.Name)) diff --git a/api/admin/cli/operations/operations.go b/api/admin/cli/operations/operations.go index 808e153..1d120c5 100644 --- a/api/admin/cli/operations/operations.go +++ b/api/admin/cli/operations/operations.go @@ -4,18 +4,18 @@ type ResourceType string const ( RealmResource ResourceType = "realm" - ClientResource = "client" - UserResource = "user" - UserFederationConfigResource = "user_federation" + ClientResource ResourceType = "client" + UserResource ResourceType = "user" + UserFederationConfigResource ResourceType = "user_federation" ) type OperationType string const ( GetOperation OperationType = "get" - CreateOperation = "create" - DeleteOperation = "delete" - UpdateOperation = "update" - ChangePassword = "change_password" - ResetPassword = "reset_password" + CreateOperation OperationType = "create" + DeleteOperation OperationType = "delete" + UpdateOperation OperationType = "update" + ChangePassword OperationType = "change_password" + ResetPassword OperationType = "reset_password" ) diff --git a/api/rest/web_api_handler.go b/api/rest/web_api_handler.go index 70c8c84..d38b157 100644 --- a/api/rest/web_api_handler.go +++ b/api/rest/web_api_handler.go @@ -89,7 +89,7 @@ func (wCtx *WebApiContext) IssueNewToken(respWriter http.ResponseWriter, request issueTokens := false // 0. Check whether we deal with issuing a new token or refresh previous one isRefresh := isTokenRefreshRequest(&tokenGenerationData) - if isRefresh == true { + if isRefresh { // 1-2. Validate refresh token and check is it fresh enough session := (*wCtx.Security).GetSessionByRefreshToken(realm, &tokenGenerationData.RefreshToken) if session == nil { @@ -192,7 +192,7 @@ func (wCtx *WebApiContext) GetUserInfo(respWriter http.ResponseWriter, request * realm := vars[globals.RealmPathVar] if !Validate(realm) { wCtx.Logger.Debug(sf.Format("Get UserInfo: is invalid realmName: '{0}'", realm)) - status := http.StatusBadRequest + status = http.StatusBadRequest result := dto.ErrorDetails{Msg: sf.Format(errors.InvalidRealm, realm)} afterHandle(&respWriter, status, &result) return @@ -240,7 +240,6 @@ func (wCtx *WebApiContext) GetUserInfo(respWriter http.ResponseWriter, request * result = dto.ErrorDetails{Msg: errors.InvalidTokenMsg, Description: errors.InvalidTokenDesc} } else { user, _ := (*wCtx.DataProvider).GetUserById(realmPtr.Name, session.UserId) - status = http.StatusOK if user != nil { result = user.GetUserInfo() } @@ -434,6 +433,7 @@ func isTokenRefreshRequest(tokenIssueData *dto.TokenGenerationData) bool { } // reserved for future use +// nolint unused func getUserIP(r *http.Request) string { IPAddress := r.Header.Get("X-Real-Ip") if IPAddress == "" { diff --git a/api/routing_fuzzing_test.go b/api/routing_fuzzing_test.go index 280e31c..a7ed26b 100644 --- a/api/routing_fuzzing_test.go +++ b/api/routing_fuzzing_test.go @@ -1,6 +1,7 @@ package api import ( + "context" "encoding/base64" "encoding/json" "io" @@ -74,9 +75,9 @@ func FuzzTestIssueNewTokenWithWrongClientId(f *testing.F) { f.Add("") f.Add("0") f.Add("00") + f.Fuzz(func(t *testing.T, clientId string) { initApp(t) - t.Parallel() issueNewToken(t, clientId, testClient1Secret, "vano", "1234567890", 400) }) } @@ -85,9 +86,9 @@ func FuzzTestIssueNewTokenWithWrongClientSecret(f *testing.F) { f.Add("\x00fb6Z4RsOadVycQoeQiN57xpu8w8wplYz") f.Add("fb6Z4RsOadVycQoeQiN57xpu8w8wplYz_!") f.Add("") + f.Fuzz(func(t *testing.T, clientSecret string) { initApp(t) - t.Parallel() issueNewToken(t, testClient1, clientSecret, "vano", "1234567890", 400) }) } @@ -96,9 +97,9 @@ func FuzzTestIssueNewTokenWithWrongUsername(f *testing.F) { f.Add("\x00vano") f.Add("!") f.Add("") + f.Fuzz(func(t *testing.T, username string) { initApp(t) - t.Parallel() issueNewToken(t, testClient1, testClient1Secret, username, "1234567890", 401) }) } @@ -107,9 +108,9 @@ func FuzzTestIssueNewTokenWithWrongPassword(f *testing.F) { f.Add("\x001234567890") f.Add("!") f.Add("") + f.Fuzz(func(t *testing.T, password string) { initApp(t) - t.Parallel() issueNewToken(t, testClient1, testClient1Secret, "vano", password, 401) }) } @@ -118,9 +119,9 @@ func FuzzTestIntrospectTokenWithWrongClientId(f *testing.F) { f.Add("\x001234567890") f.Add("!") f.Add("") + f.Fuzz(func(t *testing.T, clientId string) { initApp(t) - t.Parallel() token := getToken(t) checkIntrospectToken(t, token.AccessToken, clientId, testClient1Secret, testRealm1, 401) }) @@ -130,9 +131,9 @@ func FuzzTestIntrospectTokenWithWrongSecret(f *testing.F) { f.Add("\x001234567890") f.Add("!") f.Add("") + f.Fuzz(func(t *testing.T, clientSecret string) { initApp(t) - t.Parallel() token := getToken(t) checkIntrospectToken(t, token.AccessToken, testClient1, clientSecret, testRealm1, 401) }) @@ -143,9 +144,9 @@ func FuzzTestIntrospectTokenWithWrongToken(f *testing.F) { f.Add("\x001234567890") f.Add("!") f.Add("") + f.Fuzz(func(t *testing.T, token string) { initApp(t) - t.Parallel() checkIntrospectToken(t, token, testClient1, testClient1Secret, testRealm1, 401) }) } @@ -156,9 +157,9 @@ func FuzzTestRefreshTokenWithWrongToken(f *testing.F) { f.Add("") f.Add("0") f.Add("00") + f.Fuzz(func(t *testing.T, token string) { initApp(t) - t.Parallel() refreshToken(t, testClient1, testClient1Secret, token, 401) }) } @@ -168,14 +169,13 @@ func FuzzTestGetUserInfoWithWrongToken(f *testing.F) { f.Add("00") f.Add(" ") f.Add("\n\n") + f.Fuzz(func(t *testing.T, token string) { + initApp(t) expectedStatusCode := 401 if !isTokenValid(t, token) || len(token) == 0 { expectedStatusCode = 400 } - initApp(t) - t.Parallel() - t.Helper() userInfoUrlTemplate := "{0}/auth/realms/{1}/protocol/openid-connect/userinfo/" doRequest( t, "GET", userInfoUrlTemplate, testRealm1, nil, @@ -188,7 +188,8 @@ func initApp(t *testing.T) application.AppRunner { t.Helper() app := application.CreateAppWithData(&httpAppConfig, &testServerData, testKey, true) t.Cleanup(func() { - app.Stop() + _, err := app.Stop(context.Background()) + require.NoError(t, err) }) res, err := app.Init() assert.True(t, res) @@ -229,12 +230,11 @@ func setGetTokenFormData(clientId, clientSecret, grantType, username, password, func doPostForm(t *testing.T, reqUrl string, urlData url.Values, expectedStatus int) *http.Response { t.Helper() - response, _ := http.PostForm(reqUrl, urlData) + response, err := http.PostForm(reqUrl, urlData) + require.NoError(t, err) if response != nil { require.Equal(t, response.StatusCode, expectedStatus) } - // todo(yurishang): sometimes there is an Net Op error when running a fuzz test - // in line 'response, _ := http.PostForm(reqUrl, urlData)' return response } @@ -309,6 +309,7 @@ func getDataFromResponse[TR dto.Token | dto.ErrorDetails](t *testing.T, response func isTokenValid(t *testing.T, token string) bool { // Checking that the token doesn't contains space characters only. // If yes, then the token is not valid - the expected status code is 400. Otherwise - 401. + t.Helper() pattern := "[ \n\t]+" match, _ := regexp.MatchString(pattern, token) return !match diff --git a/application/application.go b/application/application.go index 585523c..1107b14 100644 --- a/application/application.go +++ b/application/application.go @@ -1,14 +1,15 @@ package application import ( + "context" "errors" "fmt" "io" - "io/ioutil" "net" "net/http" "os" "path/filepath" + "time" httpSwagger "github.com/swaggo/http-swagger" "github.com/wissance/Ferrum/globals" @@ -43,6 +44,8 @@ type Application struct { webApiContext *rest.WebApiContext logger *logging.AppLogger httpHandler *http.Handler + httpServer *http.Server + shutdownTimeout time.Duration } // CreateAppWithConfigs creates but not Init new Application as AppRunner @@ -82,13 +85,10 @@ func CreateAppWithData(appConfig *config.AppConfig, serverData *data.ServerData, * Return start result (true if Start was successful) and error (nil if start was successful) */ func (app *Application) Start() (bool, error) { - var err error - go func() { - err = app.startWebService() - if err != nil { - app.logger.Error(stringFormatter.Format("An error occurred during API Service Start")) - } - }() + err := app.startWebService() + if err != nil { + app.logger.Error(stringFormatter.Format("An error occurred during API Service Start")) + } return err == nil, err } @@ -150,6 +150,9 @@ func (app *Application) Init() (bool, error) { app.logger.Error(stringFormatter.Format("An error occurred during rest api init: {0}", err.Error())) return false, err } + + app.httpServer = &http.Server{Handler: *app.httpHandler} + app.shutdownTimeout = 5 * time.Second return true, nil } @@ -158,7 +161,13 @@ func (app *Application) Init() (bool, error) { * Parameters : no * Returns result of app stop and error */ -func (app *Application) Stop() (bool, error) { +func (app *Application) Stop(ctx context.Context) (bool, error) { + ctx, cancel := context.WithTimeout(ctx, app.shutdownTimeout) + defer cancel() + err := app.httpServer.Shutdown(ctx) + if err != nil { + return false, err + } return true, nil } @@ -287,21 +296,32 @@ func (app *Application) startWebService() error { var err error addressTemplate := "{0}:{1}" address := stringFormatter.Format(addressTemplate, app.appConfig.ServerCfg.Address, app.appConfig.ServerCfg.Port) + listener, err := net.Listen("tcp", address) + if err != nil { + return err + } + app.httpServer.Addr = address switch app.appConfig.ServerCfg.Schema { //nolint:exhaustive case config.HTTP: app.logger.Info(stringFormatter.Format("Starting \"HTTP\" WEB API Service on address: \"{0}\"", address)) - err = http.ListenAndServe(address, *app.httpHandler) - if err != nil { - app.logger.Error(stringFormatter.Format("An error occurred during attempt to start \"HTTP\" WEB API Service: {0}", err.Error())) - } + go func() { + err = app.httpServer.Serve(listener) + if err != nil { + app.logger.Error( + stringFormatter.Format("An error occurred during attempt to start \"HTTP\" WEB API Service: {0}", err.Error())) + } + }() case config.HTTPS: app.logger.Info(stringFormatter.Format("Starting \"HTTPS\" REST API Service on address: \"{0}\"", address)) cert := app.appConfig.ServerCfg.Security.CertificateFile key := app.appConfig.ServerCfg.Security.KeyFile - err = http.ListenAndServeTLS(address, cert, key, *app.httpHandler) - if err != nil { - app.logger.Error(stringFormatter.Format("An error occurred during attempt tp start \"HTTPS\" REST API Service: {0}", err.Error())) - } + go func() { + err = app.httpServer.ServeTLS(listener, cert, key) + if err != nil { + app.logger.Error( + stringFormatter.Format("An error occurred during attempt tp start \"HTTPS\" REST API Service: {0}", err.Error())) + } + }() } return err } @@ -313,7 +333,7 @@ func (app *Application) readKey() []byte { return nil } - fileData, err := ioutil.ReadFile(absPath) + fileData, err := os.ReadFile(absPath) if err != nil { app.logger.Error(stringFormatter.Format("An error occurred during key file reading: {0}", err.Error())) return nil @@ -352,7 +372,7 @@ func (app *Application) getSwaggerAddress() string { if len(envAddr) > 0 { return envAddr } - + // 2. Get Address from Network Interfaces addresses, err := net.InterfaceAddrs() if err != nil { diff --git a/application/application_runner.go b/application/application_runner.go index fb17aa5..793905a 100644 --- a/application/application_runner.go +++ b/application/application_runner.go @@ -1,6 +1,8 @@ package application import ( + "context" + "github.com/wissance/Ferrum/logging" ) @@ -13,7 +15,7 @@ type AppRunner interface { // Start this function starts initialized application (must be called after Init) Start() (bool, error) // Stop function to stop application - Stop() (bool, error) + Stop(ctx context.Context) (bool, error) // Init function initializes application components Init() (bool, error) // GetLogger function that required after app initialized all components to log some additional information about application stop diff --git a/application/application_test.go b/application/application_test.go index 6a02e82..fa0f0d7 100644 --- a/application/application_test.go +++ b/application/application_test.go @@ -1,6 +1,7 @@ package application import ( + "context" "crypto/tls" "encoding/base64" "encoding/json" @@ -91,6 +92,7 @@ func TestApplicationOnHttps(t *testing.T) { } func testRunCommonTestCycleImpl(t *testing.T, appConfig *config.AppConfig, baseUrl string) { + ctx := context.Background() app := CreateAppWithData(appConfig, &testServerData, testKey, true) res, err := app.Init() assert.True(t, res) @@ -164,7 +166,7 @@ func testRunCommonTestCycleImpl(t *testing.T, appConfig *config.AppConfig, baseU response = refreshToken(t, baseUrl, realm, testClient1, testClient1Secret, token.RefreshToken) assert.Equal(t, response.Status, "200 OK") - res, err = app.Stop() + res, err = app.Stop(ctx) assert.True(t, res) assert.Nil(t, err) } diff --git a/config/logs_config.go b/config/logs_config.go index 9e9aa60..405863f 100644 --- a/config/logs_config.go +++ b/config/logs_config.go @@ -1,14 +1,13 @@ package config -//Composing structs for unmarshalling. Writer is lumberjack's setup struct. -//It's annotated for JSON out-of-the-box. -//Logrus is for logging level and log output settings. - +// Composing structs for unmarshalling. Writer is lumberjack's setup struct. +// It's annotated for JSON out-of-the-box. +// Logrus is for logging level and log output settings. type AppenderType string const ( RollingFile AppenderType = "rolling_file" - Console = "console" + Console AppenderType = "console" ) /*type GlobalConfig struct { diff --git a/data/client.go b/data/client.go index 709dbbc..edb3423 100644 --- a/data/client.go +++ b/data/client.go @@ -9,7 +9,7 @@ type ClientType string const ( Public ClientType = "public" - Confidential = "confidential" + Confidential ClientType = "confidential" ) // Client is a realm client, represents an application nad set of rules for interacting with Authorization server diff --git a/data/keycloak_user.go b/data/keycloak_user.go index 78d3935..fc296d6 100644 --- a/data/keycloak_user.go +++ b/data/keycloak_user.go @@ -83,6 +83,7 @@ func (user *KeyCloakUser) SetPassword(password string, encoder *encoding.Passwor func (user *KeyCloakUser) GetId() uuid.UUID { idStrValue := getPathStringValue[string](user.rawData, "info.sub") id, err := uuid.Parse(idStrValue) + // nolint staticcheck if err != nil { // todo(UMV): think what to do here, return error! } @@ -132,11 +133,12 @@ func (user *KeyCloakUser) GetFederationId() string { func getPathStringValue[T any](rawData interface{}, path string) T { var result T mask, err := jp.ParseString(path) + // nolint staticcheck if err != nil { // todo(UMV): log and think what to do ... } res := mask.Get(rawData) - if res != nil && len(res) == 1 { + if len(res) == 1 { result = res[0].(T) } return result diff --git a/data/keycloak_user_test.go b/data/keycloak_user_test.go index 510a121..0792613 100644 --- a/data/keycloak_user_test.go +++ b/data/keycloak_user_test.go @@ -34,7 +34,6 @@ func TestInitUserWithJsonAndCheck(t *testing.T) { for _, tCase := range testCases { t.Run(tCase.name, func(t *testing.T) { - t.Parallel() jsonStr := sf.Format(tCase.userTemplate, tCase.userName, tCase.preferredUsername) var rawUserData interface{} err := json.Unmarshal([]byte(jsonStr), &rawUserData) diff --git a/go.mod b/go.mod index 0da8407..6cbfd0c 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/wissance/Ferrum -go 1.19 +go 1.21 require ( github.com/go-ldap/ldap/v3 v3.4.8 @@ -44,7 +44,7 @@ require ( golang.org/x/crypto v0.27.0 // indirect golang.org/x/net v0.29.0 // indirect golang.org/x/sys v0.25.0 // indirect - golang.org/x/tools v0.25.0 // indirect + golang.org/x/tools v0.24.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index e5eb782..04c0b1e 100644 --- a/go.sum +++ b/go.sum @@ -11,7 +11,9 @@ github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7V github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= github.com/bsm/ginkgo/v2 v2.5.0 h1:aOAnND1T40wEdAtkGSkvSICWeQ8L3UASX7YVCqQx+eQ= +github.com/bsm/ginkgo/v2 v2.5.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w= github.com/bsm/gomega v1.20.0 h1:JhAwLmtRzXFTx2AkALSLa8ijZafntmhSoU63Ok18Uq8= +github.com/bsm/gomega v1.20.0/go.mod h1:JifAceMQ4crZIWYUKrlGcmbN3bqHogVTADMD2ATsbwk= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= @@ -237,7 +239,8 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= +golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= +golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -261,6 +264,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -308,8 +312,8 @@ golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= -golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= +golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= +golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/main.go b/main.go index 2508d98..c460bf5 100644 --- a/main.go +++ b/main.go @@ -6,6 +6,7 @@ package main import ( + "context" "flag" "fmt" "os" @@ -18,8 +19,10 @@ import ( const defaultConfig = "./config.json" -var configFile = flag.String("config", defaultConfig, "--config ./config_w_redis.json") -var devMode = flag.Bool("devmode", false, "-devmode") +var ( + configFile = flag.String("config", defaultConfig, "--config ./config_w_redis.json") + devMode = flag.Bool("devmode", false, "-devmode") +) // main is an authorization server entry point is starts and stops by signal Application /* Ferrum requires config to run via cmd line, if no config was provided defaultConfig is using @@ -35,6 +38,8 @@ func main() { done := make(chan bool, 1) signal.Notify(osSignal, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) + ctx := context.Background() + app := application.CreateAppWithConfigs(*configFile, *devMode) _, initErr := app.Init() if initErr != nil { @@ -62,7 +67,7 @@ func main() { // server was started in separate goroutine, main thread is waiting for signal to stop <-done - res, err = app.Stop() + res, err = app.Stop(ctx) if !res { msg := stringFormatter.Format("An error occurred during stopping application, error is: {0}", err.Error()) fmt.Println(msg) diff --git a/managers/files/manager.go b/managers/files/manager.go index 3999d50..65695ec 100644 --- a/managers/files/manager.go +++ b/managers/files/manager.go @@ -19,8 +19,8 @@ type objectType string const ( Realm objectType = "realm" - Client = "client" - User = "user" + Client objectType = "client" + User objectType = "user" ) // FileDataManager is the simplest Data Storage without any dependencies, it uses single JSON file (it is users and clients RO auth server) @@ -110,7 +110,7 @@ func (mn *FileDataManager) GetUsers(realmName string) ([]data.User, error) { return users, nil } } - return nil, errors.NewObjectNotFoundError(User, "", sf.Format("get realm: {0} users", realmName)) + return nil, errors.NewObjectNotFoundError(string(User), "", sf.Format("get realm: {0} users", realmName)) } // GetClient function for getting Realm Client by name @@ -135,7 +135,7 @@ func (mn *FileDataManager) GetClient(realmName string, clientName string) (*data return &c, nil } } - return nil, errors.NewObjectNotFoundError(Client, clientName, sf.Format("realm: {0}", realmName)) + return nil, errors.NewObjectNotFoundError(string(Client), clientName, sf.Format("realm: {0}", realmName)) } // GetUser function for getting Realm User by userName @@ -159,7 +159,7 @@ func (mn *FileDataManager) GetUser(realmName string, userName string) (data.User return u, nil } } - return nil, errors.NewObjectNotFoundError(User, userName, sf.Format("realm: {0}", realmName)) + return nil, errors.NewObjectNotFoundError(string(User), userName, sf.Format("realm: {0}", realmName)) } // GetUserById function for getting Realm User by UserId (uuid) @@ -179,7 +179,7 @@ func (mn *FileDataManager) GetUserById(realmName string, userId uuid.UUID) (data return u, nil } } - return nil, errors.NewObjectNotFoundError(User, userId.String(), sf.Format("realm: {0}", realmName)) + return nil, errors.NewObjectNotFoundError(string(User), userId.String(), sf.Format("realm: {0}", realmName)) } // CreateRealm creates new data.Realm in a data store, receive realmData unmarshalled json in a data.Realm diff --git a/managers/files/manager_test.go b/managers/files/manager_test.go index ebb077f..b38147c 100644 --- a/managers/files/manager_test.go +++ b/managers/files/manager_test.go @@ -119,6 +119,7 @@ func checkRealm(t *testing.T, expected *data.Realm, actual *data.Realm) { assert.Equal(t, expected.RefreshTokenExpiration, actual.RefreshTokenExpiration) } +// nolint unused func checkClients(t *testing.T, expected *[]data.Client, actual *[]data.Client) { assert.Equal(t, len(*expected), len(*actual)) for _, e := range *expected { @@ -142,6 +143,7 @@ func checkClient(t *testing.T, expected *data.Client, actual *data.Client) { assert.Equal(t, expected.Auth.Value, actual.Auth.Value) } +// nolint unused func checkUsers(t *testing.T, expected *[]data.User, actual *[]data.User) { assert.Equal(t, len(*expected), len(*actual)) for _, e := range *expected { diff --git a/managers/redis/manager.go b/managers/redis/manager.go index f73d9ed..4d3873f 100644 --- a/managers/redis/manager.go +++ b/managers/redis/manager.go @@ -30,11 +30,11 @@ type objectType string const ( Realm objectType = "realm" - RealmClients = "realm clients" - RealmUsers = "realm users" - RealmUserFederationConfig = " realm user federation config" - Client = "client" - User = "user" + RealmClients objectType = "realm clients" + RealmUsers objectType = "realm users" + RealmUserFederationConfig objectType = " realm user federation config" + Client objectType = "client" + User objectType = "user" ) const defaultNamespace = "fe" @@ -253,7 +253,7 @@ func getMultipleRedisObjects[T any](redisClient *redis.Client, ctx context.Conte ) ([]T, error) { redisCmd := redisClient.MGet(ctx, objKey...) if redisCmd.Err() != nil { - //todo(UMV): print when this will be done https://github.com/Wissance/stringFormatter/issues/14 + // todo(UMV): print when this will be done https://github.com/Wissance/stringFormatter/issues/14 logger.Warn(sf.Format("An error occurred during fetching {0}: from Redis server", objName)) return nil, redisCmd.Err() } @@ -353,7 +353,8 @@ func getObjectsListOfNonSlicesItemsFromRedis[T any](redisClient *redis.Client, c } func updateObjectListItemInRedis[T any](redisClient *redis.Client, ctx context.Context, logger *logging.AppLogger, - objName objectType, objKey string, index int64, item T) error { + objName objectType, objKey string, index int64, item T, +) error { redisCmd := redisClient.LSet(ctx, objKey, index, item) if redisCmd.Err() != nil { logger.Warn(sf.Format("An error occurred during setting (update) item in LIST with key: \"{0}\" of type \"{1}\" with index {2}, error: {3}", diff --git a/managers/redis/manager_client_operations.go b/managers/redis/manager_client_operations.go index 03b709e..48f16c6 100644 --- a/managers/redis/manager_client_operations.go +++ b/managers/redis/manager_client_operations.go @@ -3,6 +3,7 @@ package redis import ( "encoding/json" "errors" + "github.com/wissance/Ferrum/config" "github.com/wissance/Ferrum/data" errors2 "github.com/wissance/Ferrum/errors" @@ -86,7 +87,7 @@ func (mn *RedisDataManager) CreateClient(realmName string, clientNew data.Client // TODO(SIA) use function isExists _, err = mn.GetClient(realmName, clientNew.Name) if err == nil { - return errors2.NewObjectExistsError(Client, clientNew.Name, sf.Format("realm: {0}", realmName)) + return errors2.NewObjectExistsError(string(Client), clientNew.Name, sf.Format("realm: {0}", realmName)) } if !errors.As(err, &errors2.ObjectNotFoundError{}) { return err @@ -207,6 +208,7 @@ func (mn *RedisDataManager) getRealmClients(realmName string) ([]data.ExtendedId * - clientName * Returns: *ExtendedIdentifier, error */ +// nolint unused func (mn *RedisDataManager) getRealmClient(realmName string, clientName string) (*data.ExtendedIdentifier, error) { realmClients, err := mn.getRealmClients(realmName) if err != nil { @@ -282,7 +284,7 @@ func (mn *RedisDataManager) createRealmClients(realmName string, realmClients [] if isAllPreDelete { if delErr := mn.deleteRealmClientsObject(realmName); delErr != nil { // todo(UMV): errors.Is because ErrZeroLength doesn't have custom type - if delErr != nil && !errors.Is(delErr, errors2.ErrNotExists) { + if !errors.Is(delErr, errors2.ErrNotExists) { return errors2.NewUnknownError("deleteRealmClientsObject", "RedisDataManager.createRealmClients", delErr) } } @@ -341,7 +343,7 @@ func (mn *RedisDataManager) deleteClientFromRealm(realmName string, clientName s } } if !isHasClient { - return errors2.NewObjectNotFoundError(Client, clientName, sf.Format("realm: {0}", realmName)) + return errors2.NewObjectNotFoundError(string(Client), clientName, sf.Format("realm: {0}", realmName)) } if createClientErr := mn.createRealmClients(realmName, realmClients, true); createClientErr != nil { return errors2.NewUnknownError("createRealmClients", "RedisDataManager.deleteClientFromRealm", createClientErr) diff --git a/managers/redis/manager_test.go b/managers/redis/manager_test.go index 838a79f..ae535cf 100644 --- a/managers/redis/manager_test.go +++ b/managers/redis/manager_test.go @@ -54,7 +54,7 @@ func TestCreateRealmSuccessfully(t *testing.T) { Value: uuid.New().String(), }, } - realm.Clients = append([]data.Client{client}) + realm.Clients = append(realm.Clients, client) } for _, u := range tCase.users { @@ -62,7 +62,7 @@ func TestCreateRealmSuccessfully(t *testing.T) { var rawUser interface{} err := json.Unmarshal([]byte(userJson), &rawUser) assert.NoError(t, err) - realm.Users = append([]interface{}{rawUser}) + realm.Users = append(realm.Users, rawUser) } err := manager.CreateRealm(realm) @@ -125,8 +125,10 @@ func TestCreateRealmWithFederationSuccessfully(t *testing.T) { err := manager.CreateRealm(realm) assert.NoError(t, err) r, err := manager.GetRealm(realm.Name) + assert.NoError(t, err) checkRealm(t, &realm, r) err = manager.DeleteRealm(realm.Name) + assert.NoError(t, err) userFederationConfigs, err := manager.GetUserFederationConfigs(realm.Name) assert.ErrorIs(t, err, appErrs.ErrZeroLength) assert.Nil(t, userFederationConfigs) @@ -179,13 +181,13 @@ func TestUpdateRealmSuccessfully(t *testing.T) { Value: uuid.New().String(), }, } - realm.Clients = append([]data.Client{client}) + realm.Clients = append(realm.Clients, client) userJson := sf.Format(`{"info":{"preferred_username":"{0}"}}`, "new_app_user") var rawUser interface{} err = json.Unmarshal([]byte(userJson), &rawUser) assert.NoError(t, err) - realm.Users = append([]interface{}{rawUser}) + realm.Users = append(realm.Users, rawUser) err = manager.UpdateRealm(prevRealmName, realm) assert.NoError(t, err) @@ -855,6 +857,7 @@ func TestCreateUserFederationServiceConfigSuccessfully(t *testing.T) { err := manager.CreateRealm(realm) assert.NoError(t, err) r, err := manager.GetRealm(realm.Name) + assert.NoError(t, err) checkRealm(t, &realm, r) // Creation of sample UserFederationService @@ -901,6 +904,7 @@ func TestUpdateUserFederationServiceConfigSuccessfully(t *testing.T) { err := manager.CreateRealm(realm) assert.NoError(t, err) r, err := manager.GetRealm(realm.Name) + assert.NoError(t, err) checkRealm(t, &realm, r) // Creation of sample UserFederationService @@ -954,6 +958,7 @@ func TestDeleteUserFederationServiceConfigSuccessfully(t *testing.T) { err := manager.CreateRealm(realm) assert.NoError(t, err) r, err := manager.GetRealm(realm.Name) + assert.NoError(t, err) checkRealm(t, &realm, r) // Creation of sample UserFederationService diff --git a/managers/redis/manager_user_federation_service_operations.go b/managers/redis/manager_user_federation_service_operations.go index 243860c..b22ef2c 100644 --- a/managers/redis/manager_user_federation_service_operations.go +++ b/managers/redis/manager_user_federation_service_operations.go @@ -3,6 +3,7 @@ package redis import ( "encoding/json" "errors" + "github.com/wissance/Ferrum/config" "github.com/wissance/Ferrum/data" appErrs "github.com/wissance/Ferrum/errors" @@ -69,7 +70,7 @@ func (mn *RedisDataManager) CreateUserFederationConfig(realmName string, userFed // TODO(UMV): use function isExists cfg, err := mn.GetUserFederationConfig(realmName, userFederationConfig.Name) if cfg != nil { - return appErrs.NewObjectExistsError(RealmUserFederationConfig, userFederationConfig.Name, sf.Format("realm: {0}", realmName)) + return appErrs.NewObjectExistsError(string(RealmUserFederationConfig), userFederationConfig.Name, sf.Format("realm: {0}", realmName)) } if !errors.As(err, &appErrs.ObjectNotFoundError{}) { return err @@ -202,7 +203,7 @@ func (mn *RedisDataManager) updateUserFederationConfigObject(realmName string, u } } - return appErrs.NewObjectNotFoundError(RealmUserFederationConfig, userFederationName, sf.Format("Realm: {0}", realmName)) + return appErrs.NewObjectNotFoundError(string(RealmUserFederationConfig), userFederationName, sf.Format("Realm: {0}", realmName)) } // deleteUserFederationConfigObject - deleting a data.UserFederationServiceConfig diff --git a/managers/redis/manager_user_operations.go b/managers/redis/manager_user_operations.go index ac4dbbd..a2520d2 100644 --- a/managers/redis/manager_user_operations.go +++ b/managers/redis/manager_user_operations.go @@ -138,7 +138,7 @@ func (mn *RedisDataManager) CreateUser(realmName string, userNew data.User) erro // TODO(SIA) use function isExists _, err = mn.GetUser(realmName, userName) if err == nil { - return errors2.NewObjectExistsError(User, userName, sf.Format("realm: {0}", realmName)) + return errors2.NewObjectExistsError(string(User), userName, sf.Format("realm: {0}", realmName)) } if !errors.As(err, &errors2.EmptyNotFoundErr) { mn.logger.Warn(sf.Format("CreateUser: GetUser failed, error: {0}", err.Error())) @@ -280,6 +280,7 @@ func (mn *RedisDataManager) getRealmUsers(realmName string) ([]data.ExtendedIden * - userName * Returns: *ExtendedIdentifier, error */ +// nolint unused func (mn *RedisDataManager) getRealmUser(realmName string, userName string) (*data.ExtendedIdentifier, error) { realmUsers, err := mn.getRealmUsers(realmName) if err != nil { @@ -297,7 +298,7 @@ func (mn *RedisDataManager) getRealmUser(realmName string, userName string) (*da } if !userFound { mn.logger.Debug(sf.Format("User with name: \"{0}\" was not found for realm: \"{1}\"", userName, realmName)) - return nil, errors2.NewObjectNotFoundError(User, userName, sf.Format("realm: {0}", realmName)) + return nil, errors2.NewObjectNotFoundError(string(User), userName, sf.Format("realm: {0}", realmName)) } return &user, nil } @@ -316,7 +317,7 @@ func (mn *RedisDataManager) getRealmUserById(realmName string, userId uuid.UUID) return nil, err } if errors.Is(err, errors2.ErrZeroLength) { - return nil, errors2.NewObjectNotFoundError(User, userId.String(), sf.Format("realm: {0}", realmName)) + return nil, errors2.NewObjectNotFoundError(string(User), userId.String(), sf.Format("realm: {0}", realmName)) } return nil, errors2.NewUnknownError("getRealmUsers", "RedisDataManager.getRealmUserById", err) } @@ -331,7 +332,7 @@ func (mn *RedisDataManager) getRealmUserById(realmName string, userId uuid.UUID) } if !userFound { mn.logger.Debug(sf.Format("User with id: \"{0}\" was not found for realm: \"{1}\"", userId, realmName)) - return nil, errors2.NewObjectNotFoundError(User, userId.String(), sf.Format("realm: {0}", realmName)) + return nil, errors2.NewObjectNotFoundError(string(User), userId.String(), sf.Format("realm: {0}", realmName)) } return &user, nil } @@ -392,7 +393,7 @@ func (mn *RedisDataManager) createRealmUsers(realmName string, realmUsers []data if isAllPreDelete { if deleteRealmUserErr := mn.deleteRealmUsersObject(realmName); deleteRealmUserErr != nil { // todo(UMV): errors.Is because ErrNotExists doesn't have custom type - if deleteRealmUserErr != nil && !errors.Is(deleteRealmUserErr, errors2.ErrNotExists) { + if !errors.Is(deleteRealmUserErr, errors2.ErrNotExists) { return errors2.NewUnknownError("deleteRealmUsersObject", "RedisDataManager.createRealmUsers", deleteRealmUserErr) } } @@ -450,7 +451,7 @@ func (mn *RedisDataManager) deleteUserFromRealm(realmName string, userName strin } } if !isHasUser { - return errors2.NewObjectNotFoundError(User, userName, sf.Format("realm: {0}", realmName)) + return errors2.NewObjectNotFoundError(string(User), userName, sf.Format("realm: {0}", realmName)) } if createRealmUserErr := mn.createRealmUsers(realmName, realmUsers, true); createRealmUserErr != nil { return errors2.NewUnknownError("createRealmUsers", "RedisDataManager.deleteUserFromRealm", createRealmUserErr) diff --git a/services/federation/ldap_federation_service.go b/services/federation/ldap_federation_service.go index b75c5c0..fee1faa 100644 --- a/services/federation/ldap_federation_service.go +++ b/services/federation/ldap_federation_service.go @@ -2,6 +2,7 @@ package federation import ( "errors" + "github.com/go-ldap/ldap/v3" "github.com/wissance/Ferrum/data" appErrs "github.com/wissance/Ferrum/errors" @@ -62,7 +63,7 @@ func (s *LdapUserFederation) GetUser(userName string, mask string) (data.User, e } if result != nil { - if result.Entries == nil || len(result.Entries) == 0 { + if len(result.Entries) == 0 { return nil, appErrs.NewFederatedUserNotFound(string(s.config.Type), s.config.Name, s.config.Url, userName) } @@ -72,7 +73,7 @@ func (s *LdapUserFederation) GetUser(userName string, mask string) (data.User, e } // todo(UMV): convert []Attributes to Json and pass - //result.Entries[0].Attributes[0].Name + // result.Entries[0].Attributes[0].Name return nil, nil } @@ -86,5 +87,4 @@ func (s *LdapUserFederation) Authenticate(userName string, password string) (boo } func (s *LdapUserFederation) Init() { - }