Refactor previous COOPGO Identity service - Initial commit

This commit is contained in:
2022-08-02 12:26:28 +02:00
commit 3e93e6593d
41 changed files with 9026 additions and 0 deletions

120
storage/etcd.go Normal file
View File

@@ -0,0 +1,120 @@
package storage
import (
"context"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/spf13/viper"
clientv3 "go.etcd.io/etcd/client/v3"
"go.etcd.io/etcd/client/v3/namespace"
)
type EtcdKVStore struct {
Client *clientv3.Client
}
func NewEtcdKVStore(cfg *viper.Viper) (EtcdKVStore, error) {
var (
endpoints = cfg.GetStringSlice("storage.kv.etcd.endpoints")
prefix = cfg.GetString("storage.kv.etcd.prefix")
)
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: 5 * time.Second,
})
if err != nil {
return EtcdKVStore{}, err
}
cli.KV = namespace.NewKV(cli.KV, prefix)
cli.Watcher = namespace.NewWatcher(cli.Watcher, prefix)
cli.Lease = namespace.NewLease(cli.Lease, prefix)
return EtcdKVStore{
Client: cli,
}, nil
}
func (s EtcdKVStore) Put(k string, v any) error {
// var data bytes.Buffer // Stand-in for a network connection
// enc := gob.NewEncoder(&data)
// err := enc.Encode(v)
// if err != nil {
// return err
// }
data, err := json.Marshal(v)
if err != nil {
return err
}
// _, err = s.Client.KV.Put(context.TODO(), k, data.String())
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
_, err = s.Client.KV.Put(ctx, k, string(data))
cancel()
if err != nil {
return err
}
return nil
}
func (s EtcdKVStore) PutWithTTL(k string, v any, duration time.Duration) error {
lease, err := s.Client.Lease.Grant(context.TODO(), int64(duration.Seconds()))
if err != nil {
return err
}
// var data bytes.Buffer // Stand-in for a network connection
// enc := gob.NewEncoder(&data)
// err = enc.Encode(v)
// if err != nil {
// return err
// }
data, err := json.Marshal(v)
if err != nil {
return err
}
// _, err = s.Client.KV.Put(context.TODO(), k, data.String(), clientv3.WithLease(lease.ID))
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
_, err = s.Client.KV.Put(ctx, k, string(data), clientv3.WithLease(lease.ID))
cancel()
if err != nil {
return err
}
return nil
}
func (s EtcdKVStore) Get(k string) (any, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
resp, err := s.Client.KV.Get(ctx, k)
cancel()
if err != nil {
return nil, err
}
for _, v := range resp.Kvs {
var data any
// var reader bytes.Buffer
// reader.Write(v.Value)
// enc := gob.NewDecoder(&reader)
// err := enc.Decode(&data)
err := json.Unmarshal([]byte(v.Value), &data)
if err != nil {
return nil, err
}
// We return directly as we want to last revision of value
return data, nil
}
return nil, errors.New(fmt.Sprintf("no value %v", k))
}
func (s EtcdKVStore) Delete(k string) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
_, err := s.Client.KV.Delete(ctx, k)
cancel()
if err != nil {
return err
}
return nil
}

142
storage/mongodb.go Normal file
View File

@@ -0,0 +1,142 @@
package storage
import (
"context"
"errors"
"github.com/spf13/viper"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
type MongoDBStorage struct {
*mongo.Client
DbName string
Collections map[string]string
}
func NewMongoDBStorage(cfg *viper.Viper) (MongoDBStorage, error) {
var (
mongodb_host = cfg.GetString("storage.db.mongodb.host")
mongodb_port = cfg.GetString("storage.db.mongodb.port")
mongodb_dbname = cfg.GetString("storage.db.mongodb.db_name")
mongodb_users = cfg.GetString("storage.db.mongodb.collections.users")
)
client, err := mongo.NewClient(options.Client().ApplyURI("mongodb://" + mongodb_host + ":" + mongodb_port))
if err != nil {
return MongoDBStorage{}, err
}
err = client.Connect(context.TODO())
if err != nil {
return MongoDBStorage{}, err
}
storage := MongoDBStorage{
Client: client,
DbName: mongodb_dbname,
Collections: map[string]string{
"users": mongodb_users,
},
}
//TODO Indexes
return storage, err
}
func (s MongoDBStorage) GetAccount(id string) (*Account, error) {
collection := s.Client.Database(s.DbName).Collection(s.Collections["users"])
account := &Account{}
if err := collection.FindOne(context.TODO(), bson.M{"_id": id}).Decode(account); err != nil {
return nil, err
}
return account, nil
}
// LocalAuthentication returns an Account matching with one of username, email or password.
// If username, is provided (not an empty string), it will search by username only
// If username is an empty string and email is provided, it will search by email
// If both username and email are empty strings, phone_number must be provided and it will search by phone number
func (s MongoDBStorage) LocalAuthentication(namespace string, username string, email string, phone_number string) (*Account, error) {
collection := s.Client.Database(s.DbName).Collection(s.Collections["users"])
account := &Account{}
if username != "" {
if err := collection.FindOne(context.TODO(), bson.M{"namespace": namespace, "authentication.local.username": username}).Decode(account); err != nil {
return nil, err
}
} else if email != "" {
if err := collection.FindOne(context.TODO(), bson.M{"namespace": namespace, "authentication.local.email": email}).Decode(account); err != nil {
return nil, err
}
} else if phone_number != "" {
if err := collection.FindOne(context.TODO(), bson.M{"namespace": namespace, "authentication.local.phone_number": phone_number}).Decode(account); err != nil {
return nil, err
}
} else {
return nil, errors.New("missing username, email or password")
}
return account, nil
}
func (s MongoDBStorage) GetAccounts(namespaces []string) (accounts []Account, err error) {
collection := s.Client.Database(s.DbName).Collection(s.Collections["users"])
var cur *mongo.Cursor
findOptions := options.Find()
if len(namespaces) == 0 {
cur, err = collection.Find(context.TODO(), bson.D{}, findOptions)
if err != nil {
return accounts, err
}
} else {
cur, err = collection.Find(context.TODO(), bson.M{"namespace": bson.M{"$in": namespaces}}, findOptions)
if err != nil {
return accounts, err
}
}
for cur.Next(context.TODO()) {
var account Account
var elem bson.M
err := cur.Decode(&elem)
if err != nil {
return accounts, err
}
bsonBytes, _ := bson.Marshal(elem)
bson.Unmarshal(bsonBytes, &account)
accounts = append(accounts, account)
}
return
}
func (s MongoDBStorage) CreateAccount(account Account) error {
collection := s.Client.Database(s.DbName).Collection(s.Collections["users"])
if _, err := collection.InsertOne(context.TODO(), account); err != nil {
return err
}
return nil
}
func (s MongoDBStorage) UpdateAccount(account Account) error {
collection := s.Client.Database(s.DbName).Collection(s.Collections["users"])
if _, err := collection.ReplaceOne(context.TODO(), bson.M{"_id": account.ID}, account); err != nil {
return err
}
return nil
}

93
storage/storage.go Normal file
View File

@@ -0,0 +1,93 @@
package storage
import (
"fmt"
"time"
"github.com/spf13/viper"
)
// Storage interface
type Storage struct {
DB DBStorage
KV KVStore
}
func NewStorage(cfg *viper.Viper) (Storage, error) {
dbstorage, err := NewDBStorage(cfg)
if err != nil {
return Storage{}, err
}
kvstore, err := NewKVStore(cfg)
if err != nil {
return Storage{}, err
}
return Storage{
DB: dbstorage,
KV: kvstore,
}, nil
}
type DBStorage interface {
GetAccount(id string) (*Account, error)
LocalAuthentication(namespace string, username string, email string, phone_number string) (*Account, error)
GetAccounts(namespaces []string) ([]Account, error)
CreateAccount(account Account) error
UpdateAccount(account Account) error
}
func NewDBStorage(cfg *viper.Viper) (DBStorage, error) {
var (
storage_type = cfg.GetString("storage.db.type")
)
switch storage_type {
case "mongodb":
s, err := NewMongoDBStorage(cfg)
return s, err
default:
return nil, fmt.Errorf("storage type %v is not supported", storage_type)
}
}
type KVStore interface {
Put(string, any) error
PutWithTTL(string, any, time.Duration) error
Get(string) (any, error)
Delete(string) error
}
func NewKVStore(cfg *viper.Viper) (KVStore, error) {
kv, err := NewEtcdKVStore(cfg)
return kv, err
}
// Data models
type Account struct {
ID string `json:"id" bson:"_id"`
Namespace string `json:"namespace"`
Authentication AccountAuth `json:"authentication" bson:"authentication"`
Data map[string]any `json:"data"`
Metadata map[string]any `json:"metadata"`
}
type AccountAuth struct {
Local LocalAuth
//TODO handle SSO
}
type LocalAuth struct {
Username string `json:"username"`
Password string `json:"password"`
Email string `json:"email"`
EmailValidation Validation `json:"email_validation" bson:"email_validation"`
PhoneNumber string `json:"phone_number" bson:"phone_number"`
PhoneNumberValidation Validation `json:"phone_number_validation" bson:"phone_number_validation"`
}
type Validation struct {
Validated bool
ValidationCode string `json:"validation_code" bson:"validation_code"`
}