diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index af0c496..195ef37 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,9 +15,9 @@ jobs: - name: Set up Go uses: actions/setup-go@v5.2.0 with: - go-version: '1.22.10' + go-version: '1.21' - name: Build - run: go version && go mod tidy && go build -v ./... + run: go version && go build -v ./... build-windows: name: Build Ferrum on windows @@ -27,9 +27,9 @@ jobs: - name: Set up Go uses: actions/setup-go@v5.2.0 with: - go-version: '1.22.10' + go-version: '1.21' - name: Build - run: go version && go mod tidy && go build -v ./... + run: go version && go build -v ./... all-tests-linux: name: Run all tests on linux @@ -39,7 +39,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5.2.0 with: - go-version: '1.22.10' + go-version: '1.21' - name: Set up Redis Stack server run: docker compose up -d redis - name: Get Redis logs @@ -70,9 +70,9 @@ jobs: - name: Set up Go uses: actions/setup-go@v5.2.0 with: - go-version: '1.22.10' + 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/api/admin/cli/main.go b/api/admin/cli/main.go index 897387c..85ec819 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" @@ -19,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() { @@ -109,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)) @@ -121,7 +122,11 @@ func main() { if err := json.Unmarshal(value, &userNew); err != nil { log.Fatalf("json.Unmarshal failed: %s", err) } - user := data.CreateUser(userNew) + realm, err := manager.GetRealm(params) + 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) } @@ -202,7 +207,7 @@ func main() { if err := json.Unmarshal(value, &newUser); err != nil { log.Fatalf("json.Unmarshal failed: %s", err) } - user := data.CreateUser(newUser) + user := data.CreateUser(newUser, nil) if err := manager.UpdateUser(params, resourceId, user); err != nil { log.Fatalf("UpdateUser failed: %s", err) } @@ -242,7 +247,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) 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 fe333ca..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" @@ -14,6 +15,7 @@ import ( "github.com/wissance/Ferrum/config" "github.com/wissance/Ferrum/data" "github.com/wissance/Ferrum/dto" + "github.com/wissance/Ferrum/utils/encoding" sf "github.com/wissance/stringFormatter" "github.com/stretchr/testify/assert" @@ -29,8 +31,11 @@ const ( ) var ( - testKey = []byte("qwerty1234567890") - testServerData = data.ServerData{ + testSalt = "salt" + encoder = encoding.NewPasswordJsonEncoder(testSalt) + testHashedPassowrd = encoder.GetB64PasswordHash("1234567890") + testKey = []byte("qwerty1234567890") + testServerData = data.ServerData{ Realms: []data.Realm{ { Name: testRealm1, TokenExpiration: testAccessTokenExpiration, RefreshTokenExpiration: testRefreshTokenExpiration, @@ -39,16 +44,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, }, }, } @@ -68,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) }) } @@ -79,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) }) } @@ -90,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) }) } @@ -101,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) }) } @@ -112,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) }) @@ -124,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) }) @@ -137,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) }) } @@ -150,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) }) } @@ -162,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, @@ -182,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) @@ -223,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 } @@ -303,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 7faec27..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" @@ -17,6 +18,7 @@ import ( "github.com/wissance/Ferrum/data" "github.com/wissance/Ferrum/dto" "github.com/wissance/Ferrum/errors" + "github.com/wissance/Ferrum/utils/encoding" "github.com/wissance/stringFormatter" ) @@ -29,8 +31,11 @@ const ( ) var ( - testKey = []byte("qwerty1234567890") - testServerData = data.ServerData{ + testSalt = "salt" + encoder = encoding.NewPasswordJsonEncoder(testSalt) + testHashedPassword = encoder.GetB64PasswordHash("1234567890") + testKey = []byte("qwerty1234567890") + testServerData = data.ServerData{ Realms: []data.Realm{ { Name: testRealm1, TokenExpiration: testAccessTokenExpiration, RefreshTokenExpiration: testRefreshTokenExpiration, @@ -39,16 +44,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": testHashedPassword}, }, }, + PasswordSalt: testSalt, }, }, } @@ -85,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) @@ -158,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.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==" } }, { 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 6f57de7..fc296d6 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" + "github.com/wissance/Ferrum/utils/encoding" ) const ( @@ -25,9 +26,14 @@ 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) + if encoder != nil { + // todo(UMV): handle CreateUser errors in the future + _ = kcUser.SetPassword(password, encoder) + } user := User(kcUser) return user } @@ -42,25 +48,29 @@ 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) + 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) +// 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) SetPassword(password string, encoder *encoding.PasswordJsonEncoder) error { + hashed := encoder.GetB64PasswordHash(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 } @@ -73,6 +83,7 @@ func (user *KeyCloakUser) SetPassword(password string) error { 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! } @@ -122,12 +133,31 @@ 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 } + +// 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..0792613 100644 --- a/data/keycloak_user_test.go +++ b/data/keycloak_user_test.go @@ -2,9 +2,11 @@ package data import ( "encoding/json" + "testing" + "github.com/stretchr/testify/assert" + "github.com/wissance/Ferrum/utils/encoding" sf "github.com/wissance/stringFormatter" - "testing" ) func TestInitUserWithJsonAndCheck(t *testing.T) { @@ -16,22 +18,28 @@ 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 { 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) 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 71721ff..cea2012 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,4 +14,6 @@ type Realm struct { TokenExpiration int `json:"token_expiration"` RefreshTokenExpiration int `json:"refresh_expiration"` UserFederationServices []UserFederationServiceConfig `json:"user_federation_services"` + PasswordSalt string `json:"password_salt"` + Encoder *encoding.PasswordJsonEncoder } diff --git a/data/user.go b/data/user.go index 228ee93..26ba1ca 100644 --- a/data/user.go +++ b/data/user.go @@ -1,13 +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 - GetPassword() string - SetPassword(password string) error + GetPasswordHash() string + SetPassword(password string, encoder *encoding.PasswordJsonEncoder) error GetId() uuid.UUID GetUserInfo() interface{} GetRawData() interface{} 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 e28fc58..65695ec 100644 --- a/managers/files/manager.go +++ b/managers/files/manager.go @@ -2,9 +2,11 @@ package files import ( "encoding/json" - "github.com/wissance/Ferrum/config" "os" + "github.com/wissance/Ferrum/config" + "github.com/wissance/Ferrum/utils/encoding" + "github.com/wissance/Ferrum/errors" "github.com/google/uuid" @@ -17,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) @@ -77,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,13 +104,13 @@ func (mn *FileDataManager) GetUsers(realmName string) ([]data.User, error) { } users := make([]data.User, len(e.Users)) for i, u := range e.Users { - user := data.CreateUser(u) + user := data.CreateUser(u, nil) users[i] = user } 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 @@ -132,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 @@ -156,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) @@ -176,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 aa2fa49..b38147c 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" @@ -67,7 +68,7 @@ func TestGetUserSuccessfully(t *testing.T) { var rawUser interface{} err := json.Unmarshal([]byte(userJson), &rawUser) assert.NoError(t, err) - expectedUser := data.CreateUser(rawUser) + expectedUser := data.CreateUser(rawUser, nil) user, err := manager.GetUser(realm, userName) assert.NoError(t, err) checkUser(t, &expectedUser, &user) @@ -96,7 +97,7 @@ func TestGetUserByIdSuccessfully(t *testing.T) { var rawUser interface{} err := json.Unmarshal([]byte(userJson), &rawUser) assert.NoError(t, err) - expectedUser := data.CreateUser(rawUser) + expectedUser := data.CreateUser(rawUser, nil) user, err := manager.GetUserById(realm, userId) assert.NoError(t, err) checkUser(t, &expectedUser, &user) @@ -118,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 { @@ -141,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 { @@ -160,5 +163,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/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.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_realm_operations.go b/managers/redis/manager_realm_operations.go index dce169c..86773ef 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" + "github.com/wissance/Ferrum/utils/encoding" sf "github.com/wissance/stringFormatter" ) @@ -47,6 +48,7 @@ func (mn *RedisDataManager) GetRealm(realmName string) (*data.Realm, error) { } } realm.UserFederationServices = configs + realm.Encoder = encoding.NewPasswordJsonEncoder(realm.PasswordSalt) return realm, nil } @@ -69,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, "") } @@ -98,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) @@ -123,6 +128,8 @@ func (mn *RedisDataManager) CreateRealm(newRealm data.Realm) error { Users: []any{}, TokenExpiration: newRealm.TokenExpiration, RefreshTokenExpiration: newRealm.RefreshTokenExpiration, + PasswordSalt: salt, + 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 73f3f60..ae535cf 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" ) @@ -53,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 { @@ -61,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) @@ -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) @@ -124,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) @@ -178,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) @@ -461,12 +464,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.GetRealm(r.Name) assert.NoError(t, err) // 2. Create multiple users users := make([]data.User, 3) @@ -478,9 +483,9 @@ func TestGetUsersSuccessfully(t *testing.T) { var rawUser interface{} err = json.Unmarshal([]byte(jsonStr), &rawUser) assert.NoError(t, err) - user := data.CreateUser(rawUser) - users[i] = user + user := data.CreateUser(rawUser, nil) err = manager.CreateUser(realm.Name, user) + users[i] = user assert.NoError(t, err) } // 3. Get all related to realm users @@ -538,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, nil) err = manager.CreateUser(realm.Name, user) assert.NoError(t, err) @@ -601,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) @@ -620,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) @@ -629,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) @@ -648,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) @@ -658,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) @@ -666,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, nil) err = manager.UpdateUser(realm.Name, userName, user) assert.NoError(t, err) @@ -695,7 +702,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, nil) err = manager.UpdateUser(realm.Name, userName, user) assert.Error(t, err) assert.True(t, errors.As(err, &appErrs.EmptyNotFoundErr)) @@ -721,7 +728,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, nil) err = manager.CreateUser(realm.Name, user) assert.NoError(t, err) u, err := manager.GetUser(realm.Name, userName) @@ -777,7 +784,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, @@ -792,19 +799,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) + + realm, err = manager.GetRealm(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, realm.Encoder) + err = manager.CreateUser(realm.Name, user) assert.NoError(t, err) // 2. Reset Password and check ... @@ -812,10 +821,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) u, err := manager.GetUser(realm.Name, userName) assert.NoError(t, err) checkUser(t, &expectedUser, &u) @@ -846,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 @@ -892,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 @@ -945,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 @@ -1057,7 +1071,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_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 d3fec05..a2520d2 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" @@ -57,7 +58,7 @@ func (mn *RedisDataManager) GetUsers(realmName string) ([]data.User, error) { userData := make([]data.User, len(realmUsersData)) for i, u := range realmUsersData { - userData[i] = data.CreateUser(u) + userData[i] = data.CreateUser(u, nil) } return userData, nil } @@ -82,7 +83,7 @@ func (mn *RedisDataManager) GetUser(realmName string, userName string) (data.Use } return nil, errors2.NewUnknownError("getSingleRedisObject", "RedisDataManager.GetUser", err) } - user := data.CreateUser(*rawUser) + user := data.CreateUser(*rawUser, nil) return user, nil } @@ -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) + _, err := mn.GetRealm(realmName) if err != nil { mn.logger.Warn(sf.Format("CreateUser: GetRealmObject failed, error: {0}", err.Error())) return err @@ -137,13 +138,12 @@ 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())) return err } - upsertUserErr := mn.upsertUserObject(realmName, userName, userNew.GetJsonString()) if upsertUserErr != nil { mn.logger.Error(sf.Format("CreateUser: addUserToRealm failed, error: {0}", upsertUserErr.Error())) @@ -241,7 +241,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.GetRealm(realmName) + if err != nil { + return errors2.NewUnknownError("GetRealm", "RedisDataManager.SetPassword", err) + } + 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 { @@ -276,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 { @@ -293,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 } @@ -312,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) } @@ -327,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 } @@ -388,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) } } @@ -446,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() { - } diff --git a/services/token_based_security.go b/services/token_based_security.go index ef4118d..a9bf1e1 100644 --- a/services/token_based_security.go +++ b/services/token_based_security.go @@ -1,9 +1,10 @@ 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" @@ -73,15 +74,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 !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/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" diff --git a/utils/encoding/encoding.go b/utils/encoding/encoding.go new file mode 100644 index 0000000..d513de0 --- /dev/null +++ b/utils/encoding/encoding.go @@ -0,0 +1,58 @@ +package encoding + +import ( + "crypto/sha512" + "encoding/base64" + "hash" + "math/rand" +) + +type PasswordJsonEncoder struct { + salt string + hasher hash.Hash +} + +func NewPasswordJsonEncoder(salt string) *PasswordJsonEncoder { + encoder := PasswordJsonEncoder{ + hasher: sha512.New(), + salt: salt, + } + return &encoder +} + +func (e *PasswordJsonEncoder) GetB64PasswordHash(password string) string { + passwordBytes := []byte(password + e.salt) + e.hasher.Write(passwordBytes) + hashedPasswordBytes := e.hasher.Sum(nil) + e.hasher.Reset() + + b64encoded := b64Encode(hashedPasswordBytes) + return b64encoded +} + +func (e *PasswordJsonEncoder) IsPasswordsMatch(password, hash string) bool { + currPasswordHash := e.GetB64PasswordHash(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 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/encoding/encoding_test.go b/utils/encoding/encoding_test.go new file mode 100644 index 0000000..8402c24 --- /dev/null +++ b/utils/encoding/encoding_test.go @@ -0,0 +1,23 @@ +package encoding + +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" + encoder := NewPasswordJsonEncoder(salt) + + // Act + hashedPwd := encoder.GetB64PasswordHash(pwd) + isMatch := encoder.IsPasswordsMatch(pwd, hashedPwd) + + // Assert + assert.True(t, isMatch) + }) +}