Skip to content
Closed

pr #6

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 66 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# Ferrum
# Ferrum .

Ferrum is a **better** Authorization Server, this is a Community version.

![GitHub go.mod Go version (subdirectory of monorepo)](https://img.shields.io/github/go-mod/go-version/wissance/Ferrum?style=plastic)
![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)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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` 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

<a href="https://github.com/Wissance/Ferrum/graphs/contributors">
<img src="https://contrib.rocks/image?repo=Wissance/Ferrum" />
Expand Down
25 changes: 15 additions & 10 deletions api/admin/cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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() {
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
Expand Down
14 changes: 10 additions & 4 deletions api/routing_fuzzing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,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"
Expand All @@ -29,8 +30,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,
Expand All @@ -39,16 +43,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,
},
},
}
Expand Down
14 changes: 10 additions & 4 deletions application/application_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,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"
)

Expand All @@ -29,8 +30,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,
Expand All @@ -39,16 +43,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,
},
},
}
Expand Down
3 changes: 2 additions & 1 deletion data.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"name": "myapp",
"token_expiration": 330,
"refresh_expiration": 200,
"password_salt": "1234567890",
"clients": [
{
"id": "d4dc483d-7d0d-4d2e-a0a0-2d34b55e5a14",
Expand All @@ -29,7 +30,7 @@
"family_name": "sys"
},
"credentials": {
"password": "1s2d3f4g90xs"
"password": "AcMWCBu5AQDN8IvSRExUUSQq7H3RH6IzsxZJqyIoEmPFtJwGknUUvzet0vhS95hgkrKLNM66v0mUB5xji8zdqA=="
}
},
{
Expand Down
58 changes: 43 additions & 15 deletions data/keycloak_user.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/google/uuid"
"github.com/ohler55/ojg/jp"
"github.com/wissance/Ferrum/utils/encoding"
)

const (
Expand All @@ -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
}
Expand All @@ -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
}
Expand Down Expand Up @@ -131,3 +141,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
}
25 changes: 17 additions & 8 deletions data/keycloak_user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -16,12 +18,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 {
Expand All @@ -31,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() {
Expand Down
Loading
Loading