working commit

This commit is contained in:
2026-06-05 18:25:03 +02:00
commit 7d8abba003
82 changed files with 21863 additions and 0 deletions
+152
View File
@@ -0,0 +1,152 @@
package config
import (
"errors"
"flag"
"fmt"
"os"
"path/filepath"
"mbase/pkg/client"
"go.yaml.in/yaml/v4"
)
const (
defaultHostname = "localhost"
configFilename = "maind.yaml"
logFilename = "maind.log"
pidFilename = "maind.pid"
)
var (
buildVersion = "NONE"
)
type Networks struct {
Enabled []string `json:"enabled" yaml:"enabled"`
Disabled []string `json:"disabled" yaml:"disabled"`
}
type ServiceConfig struct {
Portnum uint32 `json:"port" yaml:"port"`
Address string `json:"address" yaml:"address"`
Protocol string `json:"protocol" yaml:"protocol"`
}
type Config struct {
PackageVersion string `json:"packageVersion" yaml:"packageVersion"`
Service ServiceConfig `json:"service" yaml:"service"`
Networks Networks `json:"networks" yaml:"networks"`
Hostname string `json:"hostname" yaml:"hostname"`
Debug bool `json:"debug" yaml:"debug"`
Build string `json:"build" yaml:"build"`
LogPath string `json:"logfile" yaml:"logfile"`
RunPath string `json:"runfile" yaml:"runfile"`
DataDir string `json:"datadir" yaml:"datadir"`
Daemon bool `json:"daemon" yaml:"daemon"`
}
var (
defaultEnabledNetworks = []string{"0.0.0.0/0", "::/0"}
defaultDisabledNetworks = []string{}
)
const (
defaultServiceAddress = "0.0.0.0"
defaultServiceProtocol = "tcp"
)
func NewConfig() *Config {
conf := &Config{
Service: ServiceConfig{
Portnum: client.DefaultPort,
Address: defaultServiceAddress,
Protocol: defaultServiceProtocol,
},
DataDir: datadirPath,
Debug: false,
Hostname: defaultHostname,
Build: buildVersion,
Daemon: true,
PackageVersion: packageVersion,
Networks: Networks{
Enabled: defaultEnabledNetworks,
Disabled: defaultDisabledNetworks,
},
}
conf.LogPath = filepath.Join(logdirPath, logFilename)
conf.RunPath = filepath.Join(rundirPath, pidFilename)
return conf
}
func (conf *Config) ReadFile() error {
var err error
confPath := filepath.Join(confdirPath, configFilename)
confBytes, err := os.ReadFile(confPath)
if err != nil {
return err
}
err = yaml.Unmarshal(confBytes, conf)
if err != nil {
return err
}
conf.Normalize()
err = conf.Validate()
if err != nil {
return err
}
return nil
}
func (conf *Config) ReadEnv() error {
var err error
return err
}
func (conf *Config) ReadOpts() error {
var err error
exeName := filepath.Base(os.Args[0])
flag.BoolVar(&conf.Daemon, "daemon", conf.Daemon, "run as daemon")
flag.BoolVar(&conf.Debug, "debug", conf.Debug, "on debug mode")
help := func() {
fmt.Println("")
fmt.Printf("Usage: %s [option]\n", exeName)
fmt.Println("")
fmt.Println("Options:")
flag.PrintDefaults()
fmt.Println("")
}
flag.Usage = help
flag.Parse()
return err
}
func (conf *Config) String() (string, error) {
var err error
var res string
yamlBytes, err := yaml.Marshal(conf)
if err != nil {
return res, err
}
res = string(yamlBytes)
return res, err
}
func (conf *Config) Normalize() {
if conf.Service.Portnum == 0 {
conf.Service.Portnum = client.DefaultPort
}
}
func (conf *Config) Validate() error {
var err []error
return errors.Join(err...)
}
+10
View File
@@ -0,0 +1,10 @@
package config
const (
confdirPath = "@srv_confdir@"
rundirPath = "@srv_rundir@"
logdirPath = "@srv_logdir@"
datadirPath = "@srv_datadir@"
packageVersion = "@PACKAGE_VERSION@"
)
+114
View File
@@ -0,0 +1,114 @@
package database
import (
"context"
"mbase/app/descriptor"
)
func (db *Database) InsertAccount(ctx context.Context, account *descriptor.Account) error {
var err error
request := `INSERT INTO account(id, username, passhash, disabled, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6)`
_, err = db.db.Exec(request, account.ID, account.Username, account.Passhash,
account.Disabled, account.CreatedAt, account.UpdatedAt)
if err != nil {
return err
}
return err
}
func (db *Database) UpdateAccountByID(ctx context.Context, accountID int64, account *descriptor.Account) error {
var err error
request := `UPDATE account SET username = $1, passhash = $2, disabled = $3, updated_at = $4 WHERE id = $5`
_, err = db.db.Exec(request, account.Username, account.Passhash, account.Disabled, account.UpdatedAt, accountID)
if err != nil {
return err
}
return err
}
func (db *Database) ReducedListAccounts(ctx context.Context) ([]descriptor.Account, error) {
var err error
request := `SELECT id, username, disabled, created_at, updated_at FROM account`
res := make([]descriptor.Account, 0)
err = db.db.Select(&res, request)
if err != nil {
return res, err
}
return res, err
}
func (db *Database) CompletedListAccounts(ctx context.Context) ([]descriptor.Account, error) {
var err error
request := `SELECT * FROM account`
res := make([]descriptor.Account, 0)
err = db.db.Select(&res, request)
if err != nil {
return res, err
}
return res, err
}
func (db *Database) GetAccountByID(ctx context.Context, accountID int64) (bool, *descriptor.Account, error) {
var err error
var res *descriptor.Account
var exists bool
request := `SELECT id, username, passhash, disabled, created_at, updated_at
FROM account WHERE id = $1 LiMIT 1`
dbRes := make([]descriptor.Account, 0)
err = db.db.Select(&dbRes, request, accountID)
if err != nil {
return exists, res, err
}
if len(dbRes) == 0 {
return exists, res, err
}
exists = true
res = &dbRes[0]
return exists, res, err
}
func (db *Database) GetAccountByUsername(ctx context.Context, username string) (bool, *descriptor.Account, error) {
var err error
var res *descriptor.Account
var exists bool
request := `SELECT id, username, passhash, disabled, created_at, updated_at
FROM account WHERE username = $1 LIMIT 1`
dbRes := make([]descriptor.Account, 0)
err = db.db.Select(&dbRes, request, username)
if err != nil {
return exists, res, err
}
if len(dbRes) == 0 {
return false, res, err
}
exists = true
res = &dbRes[0]
return exists, res, err
}
func (db *Database) DeleteAccountByID(ctx context.Context, accountID int64) error {
var err error
request := `DELETE FROM account WHERE id = $1`
_, err = db.db.Exec(request, accountID)
if err != nil {
return err
}
return err
}
func (db *Database) DeleteAccountByUsername(ctx context.Context, username string) error {
var err error
request := `DELETE FROM account WHERE username = $1`
_, err = db.db.Exec(request, username)
if err != nil {
return err
}
return err
}
+124
View File
@@ -0,0 +1,124 @@
package database
import (
"context"
"path/filepath"
"mbase/pkg/logger"
"github.com/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3"
)
const schema = `
--- DROP TABLE IF EXISTS issuer;
CREATE TABLE IF NOT EXISTS issuer (
id INT NOT NULL,
name TEXT NOT NULL,
cert TEXT NOT NULL,
key TEXT,
signer_id INT NOT NULL,
signer_name TEXT NOT NULL,
revoked BOOL
);
CREATE UNIQUE INDEX IF NOT EXISTS issuer_index01
ON issuer(id);
CREATE UNIQUE INDEX IF NOT EXISTS issuer_index02
ON issuer(name);
--- DROP TABLE IF EXISTS service;
CREATE TABLE IF NOT EXISTS service (
id INT NOT NULL,
issuer_id INT NOT NULL,
issuer_name TEXT NOT NULL,
name TEXT NOT NULL,
cert TEXT NOT NULL,
key TEXT NOT NULL,
revoked BOOL
);
CREATE UNIQUE INDEX IF NOT EXISTS service_index01
ON service(id);
CREATE UNIQUE INDEX IF NOT EXISTS service_index02
ON service(name);
--- DROP TABLE IF EXISTS account;
CREATE TABLE IF NOT EXISTS account (
id INT NOT NULL,
username TEXT NOT NULL,
passhash TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
disabled BOOL
);
CREATE UNIQUE INDEX IF NOT EXISTS account_index01
ON account(id);
CREATE UNIQUE INDEX IF NOT EXISTS account_index02
ON account(username);
--- DROP TABLE IF EXISTS grant;
CREATE TABLE IF NOT EXISTS grant (
id INT NOT NULL,
account_id INT NOT NULL,
operation TEXT NOT NULL,
created_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS grant_index01
ON grant(account_id);
CREATE UNIQUE INDEX IF NOT EXISTS grant_index02
ON grant(account_id, operation);
`
type Database struct {
datapath string
db *sqlx.DB
log *logger.Logger
}
func NewDatabase(datapath string) (*Database, error) {
var err error
db := &Database{
datapath: datapath,
}
db.log = logger.NewLogger("database")
return db, err
}
func (db *Database) OpenDatabase() error {
var err error
dbPath := filepath.Join(db.datapath, "certmanager.db")
db.db, err = sqlx.Open("sqlite3", dbPath)
if err != nil {
return err
}
err = db.db.Ping()
if err != nil {
return err
}
return err
}
func (db *Database) InitDatabase() error {
var err error
_, err = db.db.Exec(schema)
if err != nil {
return err
}
return err
}
func (db *Database) CleanDatabase(ctx context.Context) error {
var err error
request := `
DELETE FROM account;
DELETE FROM grant;
`
_, err = db.db.Exec(request)
if err != nil {
return err
}
return err
}
+77
View File
@@ -0,0 +1,77 @@
package database
import (
"context"
"mbase/app/descriptor"
)
func (db *Database) InsertGrant(ctx context.Context, grant *descriptor.Grant) error {
var err error
request := `INSERT INTO grant(id, account_id, operation, created_at)
VALUES ($1, $2, $3, $4)`
_, err = db.db.Exec(request, grant.ID, grant.AccountID, grant.Operation, grant.CreatedAt)
if err != nil {
return err
}
return err
}
func (db *Database) ListGrantsByAccountID(ctx context.Context, accountID int64) ([]descriptor.Grant, error) {
var err error
request := `SELECT * FROM grant WHERE account_id = $1`
res := make([]descriptor.Grant, 0)
err = db.db.Select(&res, request, accountID)
if err != nil {
return res, err
}
return res, err
}
func (db *Database) ListGrants(ctx context.Context) ([]descriptor.Grant, error) {
var err error
request := `SELECT * FROM grant`
res := make([]descriptor.Grant, 0)
err = db.db.Select(&res, request)
if err != nil {
return res, err
}
return res, err
}
func (db *Database) GetGrant(ctx context.Context, accountID int64, operation string) (bool, *descriptor.Grant, error) {
var err error
res := &descriptor.Grant{}
request := `SELECT * FROM grant WHERE account_id = $1 AND operation = $2 LIMIT 1`
dbRes := make([]descriptor.Grant, 0)
err = db.db.Select(&dbRes, request, accountID, operation)
if err != nil {
return false, res, err
}
if len(dbRes) == 0 {
return false, res, err
}
res = &dbRes[0]
return true, res, err
}
func (db *Database) DeleteGrantByAccountID(ctx context.Context, grantID int64, operation string) error {
var err error
request := `DELETE FROM grant WHERE account_id = $1 AND operation = $2`
_, err = db.db.Exec(request, grantID, operation)
if err != nil {
return err
}
return err
}
func (db *Database) DeleteAllGrantsForAccountID(ctx context.Context, grantID int64) error {
var err error
request := `DELETE FROM grant WHERE account_id = $1`
_, err = db.db.Exec(request, grantID)
if err != nil {
return err
}
return err
}
+34
View File
@@ -0,0 +1,34 @@
package descriptor
const (
GrantModifyUsers = "modifyUsers"
GrantModifyDatabase = "modifyDatabase"
)
type Dump struct {
Timestamp string `json:"timestamp" yaml:"timestamp"`
Accounts []Account `json:"accounts" yaml:"accounts"`
Grants []Grant `json:"grants" yaml:"grants"`
}
type Account struct {
ID int64 `json:"id" yaml:"id" db:"id"`
Username string `json:"username" yaml:"username" db:"username"`
Passhash string `json:"passhash" yaml:"passhash" db:"passhash"`
Disabled bool `json:"disabled" yaml:"disabled" db:"disabled"`
CreatedAt string `json:"createdAt" yaml:"createdAt" db:"created_at"`
UpdatedAt string `json:"updatedAt" yaml:"updatedAt" db:"updated_at"`
}
type Grant struct {
ID int64 `json:"id" yaml:"id" db:"id"`
AccountID int64 `json:"accountID" yaml:"accountID" db:"account_id"`
Operation string `json:"operation" yaml:"operation" db:"operation"`
CreatedAt string `json:"createdAt" yaml:"createdAt" db:"created_at"`
}
type Server struct {
DatabaseInitialized bool `json:"databaseInitialized" yaml:"databaseInitialized"`
CreatedAt string `json:"createdAt" yaml:"createdAt"`
UpdatedAt string `json:"updatedAt" yaml:"updatedAt"`
}
+80
View File
@@ -0,0 +1,80 @@
package handler
import (
"context"
"mbase/pkg/mbctl"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
func (hand *Handler) Authentificate(ctx context.Context) (int64, error) {
var err error
var accountID int64
meta, _ := metadata.FromIncomingContext(ctx)
usernameArr := meta["username"]
passwordArr := meta["password"]
if len(usernameArr) == 0 || len(passwordArr) == 0 {
err := status.Errorf(codes.PermissionDenied, "Empty auth data")
return accountID, err
}
username := meta["username"][0]
password := meta["password"][0]
validated, accountID, err := hand.lg.ValidateAcount(ctx, username, password)
if !validated {
err := status.Errorf(codes.PermissionDenied, "Wrong auth data")
return accountID, err
}
return accountID, err
}
func (hand *Handler) CreateAccount(ctx context.Context, params *mbctl.CreateAccountParams) (*mbctl.CreateAccountResult, error) {
var err error
hand.log.Debugf("Handle CreateAccount call")
res := &mbctl.CreateAccountResult{}
accountID, err := hand.Authentificate(ctx)
if err != nil {
return res, err
}
res, err = hand.lg.CreateAccount(ctx, accountID, params)
return res, err
}
func (hand *Handler) DeleteAccount(ctx context.Context, params *mbctl.DeleteAccountParams) (*mbctl.DeleteAccountResult, error) {
var err error
hand.log.Debugf("Handle DeleteAccount call")
res := &mbctl.DeleteAccountResult{}
accountID, err := hand.Authentificate(ctx)
if err != nil {
return res, err
}
res, err = hand.lg.DeleteAccount(ctx, accountID, params)
return res, err
}
func (hand *Handler) ListAccounts(ctx context.Context, params *mbctl.ListAccountsParams) (*mbctl.ListAccountsResult, error) {
var err error
hand.log.Debugf("Handle ListAccounts call")
res := &mbctl.ListAccountsResult{}
accountID, err := hand.Authentificate(ctx)
if err != nil {
return res, err
}
res, err = hand.lg.ListAccounts(ctx, accountID, params)
return res, err
}
func (hand *Handler) UpdateAccount(ctx context.Context, params *mbctl.UpdateAccountParams) (*mbctl.UpdateAccountResult, error) {
var err error
hand.log.Debugf("Handle UpdateAccount call")
res := &mbctl.UpdateAccountResult{}
accountID, err := hand.Authentificate(ctx)
if err != nil {
return res, err
}
res, err = hand.lg.UpdateAccount(ctx, accountID, params)
return res, err
}
+31
View File
@@ -0,0 +1,31 @@
package handler
import (
"context"
"mbase/pkg/mbctl"
)
func (hand *Handler) GetDump(ctx context.Context, params *mbctl.GetDumpParams) (*mbctl.GetDumpResult, error) {
var err error
hand.log.Debugf("Handle GetDump call")
res := &mbctl.GetDumpResult{}
accountID, err := hand.Authentificate(ctx)
if err != nil {
return res, err
}
res, err = hand.lg.GetDump(ctx, accountID, params)
return res, err
}
func (hand *Handler) RestoreDump(ctx context.Context, params *mbctl.RestoreDumpParams) (*mbctl.RestoreDumpResult, error) {
var err error
hand.log.Debugf("Handle GetDump call")
res := &mbctl.RestoreDumpResult{}
accountID, err := hand.Authentificate(ctx)
if err != nil {
return res, err
}
res, err = hand.lg.RestoreDump(ctx, accountID, params)
return res, err
}
+31
View File
@@ -0,0 +1,31 @@
package handler
import (
"context"
"mbase/pkg/mbctl"
)
func (hand *Handler) SetGrant(ctx context.Context, params *mbctl.SetGrantParams) (*mbctl.SetGrantResult, error) {
var err error
hand.log.Debugf("Handle SetGrant call")
res := &mbctl.SetGrantResult{}
accountID, err := hand.Authentificate(ctx)
if err != nil {
return res, err
}
res, err = hand.lg.SetGrant(ctx, accountID, params)
return res, err
}
func (hand *Handler) DeleteGrant(ctx context.Context, params *mbctl.DeleteGrantParams) (*mbctl.DeleteGrantResult, error) {
var err error
hand.log.Debugf("Handle DeleteGrant call")
res := &mbctl.DeleteGrantResult{}
accountID, err := hand.Authentificate(ctx)
if err != nil {
return res, err
}
res, err = hand.lg.DeleteGrant(ctx, accountID, params)
return res, err
}
+31
View File
@@ -0,0 +1,31 @@
package handler
import (
"mbase/app/logic"
"mbase/pkg/mbctl"
"mbase/pkg/logger"
"google.golang.org/grpc"
)
type HandlerConfig struct {
Logic *logic.Logic
}
type Handler struct {
mbctl.UnimplementedControlServer
lg *logic.Logic
log *logger.Logger
}
func NewHandler(conf *HandlerConfig) *Handler {
hand := Handler{
lg: conf.Logic,
}
hand.log = logger.NewLogger("ghandler")
return &hand
}
func (hand *Handler) Register(gsrv *grpc.Server) {
mbctl.RegisterControlServer(gsrv, hand)
}
+14
View File
@@ -0,0 +1,14 @@
package handler
import (
"context"
"mbase/pkg/mbctl"
)
func (hand *Handler) GetHello(ctx context.Context, params *mbctl.GetHelloParams) (*mbctl.GetHelloResult, error) {
var err error
hand.log.Debugf("Handle getHello call")
res, err := hand.lg.GetHello(ctx, params)
return res, err
}
+232
View File
@@ -0,0 +1,232 @@
package logic
import (
"context"
"fmt"
"time"
"mbase/app/descriptor"
"mbase/pkg/auxid"
"mbase/pkg/auxpwd"
"mbase/pkg/mbctl"
)
func (lg *Logic) ValidateAcount(ctx context.Context, username, password string) (bool, int64, error) {
var err error
var accountID int64
var valid bool
lg.WaitRestoring()
accountExists, accountDescr, err := lg.db.GetAccountByUsername(ctx, username)
if !accountExists {
err := fmt.Errorf("Account not exists")
return valid, accountID, err
}
if !auxpwd.PasswordMatchCompat([]byte(password), accountDescr.Passhash) {
err := fmt.Errorf("Login data mismatch")
return valid, accountID, err
}
valid = true
accountID = accountDescr.ID
return valid, accountID, err
}
func (lg *Logic) CreateAccount(ctx context.Context, accountID int64, params *mbctl.CreateAccountParams) (*mbctl.CreateAccountResult, error) {
var err error
res := &mbctl.CreateAccountResult{}
lg.WaitDumping()
lg.WaitRestoring()
grantExists, _, err := lg.db.GetGrant(ctx, accountID, descriptor.GrantModifyUsers)
if err != nil {
return res, err
}
if !grantExists {
err := fmt.Errorf("Operation not allowed for the user")
return res, err
}
if params.Username == "" {
err := fmt.Errorf("Empty username parameters")
return res, err
}
if params.Password == "" {
err := fmt.Errorf("Empty password parameter")
return res, err
}
accountExists, _, err := lg.db.GetAccountByUsername(ctx, params.Username)
if err != nil {
return res, err
}
if accountExists {
err := fmt.Errorf("Account with thist name already exists")
return res, err
}
now := time.Now().Format(time.RFC3339)
passhash := auxpwd.MakeSHA256Hash([]byte(params.Password))
accountDescr := &descriptor.Account{
ID: auxid.GenID(),
Username: params.Username,
Passhash: passhash,
Disabled: false,
CreatedAt: now,
UpdatedAt: now,
}
err = lg.db.InsertAccount(ctx, accountDescr)
if err != nil {
return res, err
}
res.AccountID = accountDescr.ID
return res, err
}
func (lg *Logic) UpdateAccount(ctx context.Context, accountID int64, params *mbctl.UpdateAccountParams) (*mbctl.UpdateAccountResult, error) {
var err error
res := &mbctl.UpdateAccountResult{}
lg.WaitRestoring()
lg.WaitDumping()
grantExists, _, err := lg.db.GetGrant(ctx, accountID, descriptor.GrantModifyUsers)
if err != nil {
return res, err
}
if !grantExists {
err := fmt.Errorf("Operation not allowed for the user")
return res, err
}
var accountDescr *descriptor.Account
var accountExists bool
switch {
case params.AccountID != 0:
accountExists, accountDescr, err = lg.db.GetAccountByID(ctx, params.AccountID)
if err != nil {
return res, err
}
case params.Username != "":
accountExists, accountDescr, err = lg.db.GetAccountByUsername(ctx, params.Username)
if err != nil {
return res, err
}
}
if !accountExists {
err := fmt.Errorf("Account with this is or name dont exists")
return res, err
}
now := time.Now().Format(time.RFC3339)
if params.NewUsername != "" {
accountDescr.UpdatedAt = now
accountDescr.Username = params.NewUsername
}
if params.NewPassword != "" {
passhash := auxpwd.MakeSHA256Hash([]byte(params.NewPassword))
accountDescr.UpdatedAt = now
accountDescr.Passhash = passhash
}
if params.Disabled != accountDescr.Disabled {
accountDescr.UpdatedAt = now
accountDescr.Disabled = params.Disabled
}
err = lg.db.UpdateAccountByID(ctx, accountDescr.ID, accountDescr)
if err != nil {
return res, err
}
return res, err
}
func (lg *Logic) DeleteAccount(ctx context.Context, accountID int64, params *mbctl.DeleteAccountParams) (*mbctl.DeleteAccountResult, error) {
var err error
res := &mbctl.DeleteAccountResult{}
lg.WaitDumping()
lg.WaitRestoring()
grantExists, _, err := lg.db.GetGrant(ctx, accountID, descriptor.GrantModifyUsers)
if err != nil {
return res, err
}
if !grantExists {
err := fmt.Errorf("Operation not allowed for the user")
return res, err
}
var accountDescr *descriptor.Account
var accountExists bool
switch {
case params.AccountID != 0:
accountExists, accountDescr, err = lg.db.GetAccountByID(ctx, params.AccountID)
if err != nil {
return res, err
}
case params.Username != "":
accountExists, accountDescr, err = lg.db.GetAccountByUsername(ctx, params.Username)
if err != nil {
return res, err
}
}
if !accountExists {
err := fmt.Errorf("Account with this is or name dont exists")
return res, err
}
err = lg.db.DeleteAllGrantsForAccountID(ctx, accountDescr.ID)
if err != nil {
return res, err
}
err = lg.db.DeleteAccountByID(ctx, accountDescr.ID)
if err != nil {
return res, err
}
return res, err
}
func (lg *Logic) ListAccounts(ctx context.Context, accountID int64, params *mbctl.ListAccountsParams) (*mbctl.ListAccountsResult, error) {
var err error
res := &mbctl.ListAccountsResult{
Accounts: make([]*mbctl.AccountShortDescr, 0),
}
lg.WaitRestoring()
grantExists, _, err := lg.db.GetGrant(ctx, accountID, descriptor.GrantModifyUsers)
if err != nil {
return res, err
}
if !grantExists {
err := fmt.Errorf("Operation not allowed for the user")
return res, err
}
accountDescrs, err := lg.db.ReducedListAccounts(ctx)
if err != nil {
return res, err
}
for _, accountDescr := range accountDescrs {
accountShortDescr := &mbctl.AccountShortDescr{
Username: accountDescr.Username,
Disabled: accountDescr.Disabled,
CreatedAt: accountDescr.CreatedAt,
UpdatedAt: accountDescr.UpdatedAt,
Grants: make([]*mbctl.GrantShortDescr, 0),
}
grantDescrs, err := lg.db.ListGrantsByAccountID(ctx, accountDescr.ID)
if err != nil {
return res, err
}
for _, grantDescrs := range grantDescrs {
grantShortDescrs := &mbctl.GrantShortDescr{
Operation: grantDescrs.Operation,
CreatedAt: grantDescrs.CreatedAt,
}
accountShortDescr.Grants = append(accountShortDescr.Grants, grantShortDescrs)
}
res.Accounts = append(res.Accounts, accountShortDescr)
}
return res, err
}
+71
View File
@@ -0,0 +1,71 @@
package logic
import (
"context"
"time"
"mbase/app/descriptor"
"mbase/pkg/auxid"
"mbase/pkg/auxpwd"
)
const (
defaultSeedUsername = "certman"
defaultSeedPassword = "certman"
)
func (lg *Logic) CleanDatabase(ctx context.Context) error {
var err error
err = lg.db.CleanDatabase(ctx)
if err != nil {
return err
}
return err
}
func (lg *Logic) SeedAccount(ctx context.Context) (int64, error) {
var err error
var accountID int64
accountDescrs, err := lg.db.ReducedListAccounts(ctx)
if err != nil {
return accountID, err
}
lg.log.Debugf("Seed account")
if len(accountDescrs) == 0 {
now := time.Now().Format(time.RFC3339)
passhash := auxpwd.MakeSHA256Hash([]byte(defaultSeedPassword))
accountDescr := &descriptor.Account{
ID: auxid.GenID(),
Username: defaultSeedUsername,
Passhash: passhash,
Disabled: false,
CreatedAt: now,
UpdatedAt: now,
}
err = lg.db.InsertAccount(ctx, accountDescr)
if err != nil {
return accountID, err
}
accountID = accountDescr.ID
grantTypes := []string{
descriptor.GrantModifyUsers,
descriptor.GrantModifyDatabase,
}
for _, grantType := range grantTypes {
grantDescr := &descriptor.Grant{
AccountID: accountDescr.ID,
Operation: grantType,
CreatedAt: now,
}
err = lg.db.InsertGrant(ctx, grantDescr)
if err != nil {
return accountID, err
}
}
}
return accountID, err
}
+107
View File
@@ -0,0 +1,107 @@
package logic
import (
"context"
"fmt"
"time"
"mbase/app/descriptor"
"mbase/pkg/mbctl"
"go.yaml.in/yaml/v4"
)
func (lg *Logic) GetDump(ctx context.Context, accountID int64, params *mbctl.GetDumpParams) (*mbctl.GetDumpResult, error) {
var err error
res := &mbctl.GetDumpResult{}
grantExists, _, err := lg.db.GetGrant(ctx, accountID, descriptor.GrantModifyDatabase)
if err != nil {
return res, err
}
if !grantExists {
err := fmt.Errorf("Operation not allowed for the user")
return res, err
}
lg.WaitRestoring()
lg.WaitDumping()
lg.DumpingSemUp()
defer lg.DumpingSemDown()
listAccounts, err := lg.db.CompletedListAccounts(ctx)
if err != nil {
return res, err
}
listGrants, err := lg.db.ListGrants(ctx)
if err != nil {
return res, err
}
lg.DumpingSemDown()
dump := descriptor.Dump{
Timestamp: time.Now().Format(time.RFC3339),
Accounts: listAccounts,
Grants: listGrants,
}
dumpBytes, err := yaml.Marshal(dump)
if err != nil {
return res, err
}
res.Dump = string(dumpBytes)
return res, err
}
func (lg *Logic) RestoreDump(ctx context.Context, accountID int64, params *mbctl.RestoreDumpParams) (*mbctl.RestoreDumpResult, error) {
var err error
res := &mbctl.RestoreDumpResult{}
grantExists, _, err := lg.db.GetGrant(ctx, accountID, descriptor.GrantModifyDatabase)
if err != nil {
return res, err
}
if !grantExists {
err := fmt.Errorf("Operation not allowed for the user")
return res, err
}
lg.WaitDumping()
lg.WaitRestoring()
var dump descriptor.Dump
err = yaml.Unmarshal([]byte(params.Dump), &dump)
if err != nil {
return res, err
}
lg.RestoringSemUp()
defer lg.RestoringSemDown()
if params.DeleteAllRecords {
err = lg.db.CleanDatabase(ctx)
if err != nil {
return res, err
}
}
for _, account := range dump.Accounts {
lg.log.Infof("Insert account %s", account.Username)
err = lg.db.InsertAccount(ctx, &account)
if err != nil {
lg.log.Errorf("Insert account error: %v", err)
}
}
for _, grant := range dump.Grants {
lg.log.Infof("Insert grant %s for account %d", grant.Operation, grant.AccountID)
err = lg.db.InsertGrant(ctx, &grant)
if err != nil {
lg.log.Errorf("Insert account error: %v", err)
}
}
return res, err
}
+148
View File
@@ -0,0 +1,148 @@
package logic
import (
"context"
"fmt"
"time"
"mbase/app/descriptor"
"mbase/pkg/auxid"
"mbase/pkg/mbctl"
)
func (lg *Logic) SetGrant(ctx context.Context, accountID int64, params *mbctl.SetGrantParams) (*mbctl.SetGrantResult, error) {
var err error
res := &mbctl.SetGrantResult{}
lg.WaitDumping()
grantExists, _, err := lg.db.GetGrant(ctx, accountID, descriptor.GrantModifyUsers)
if err != nil {
return res, err
}
if !grantExists {
err := fmt.Errorf("Operation not allowed for the user")
return res, err
}
grantTypes := []string{
descriptor.GrantModifyUsers,
descriptor.GrantModifyDatabase,
}
var grantOk bool
for _, grantType := range grantTypes {
if grantType == params.Operation {
grantOk = true
break
}
}
if !grantOk {
err := fmt.Errorf("Unknown grant type")
return res, err
}
var accountDescr *descriptor.Account
var accountExists bool
switch {
case params.AccountID != 0:
accountExists, accountDescr, err = lg.db.GetAccountByID(ctx, params.AccountID)
if err != nil {
return res, err
}
case params.Username != "":
accountExists, accountDescr, err = lg.db.GetAccountByUsername(ctx, params.Username)
if err != nil {
return res, err
}
}
if !accountExists || accountDescr == nil {
err := fmt.Errorf("Account with this id or name dont exists")
return res, err
}
grantExists, _, err = lg.db.GetGrant(ctx, accountDescr.ID, params.Operation)
if err != nil {
return res, err
}
if grantExists {
err := fmt.Errorf("Grant %s for the user already exists", params.Operation)
return res, err
}
now := time.Now().Format(time.RFC3339)
grantDescr := &descriptor.Grant{
ID: auxid.GenID(),
AccountID: accountDescr.ID,
CreatedAt: now,
Operation: params.Operation,
}
err = lg.db.InsertGrant(ctx, grantDescr)
if err != nil {
return res, err
}
return res, err
}
func (lg *Logic) DeleteGrant(ctx context.Context, accountID int64, params *mbctl.DeleteGrantParams) (*mbctl.DeleteGrantResult, error) {
var err error
res := &mbctl.DeleteGrantResult{}
lg.WaitDumping()
grantExists, _, err := lg.db.GetGrant(ctx, accountID, descriptor.GrantModifyUsers)
if err != nil {
return res, err
}
if !grantExists {
err := fmt.Errorf("Operation not allowed for the user")
return res, err
}
grantTypes := []string{
descriptor.GrantModifyUsers,
descriptor.GrantModifyDatabase,
}
var grantOk bool
for _, grantType := range grantTypes {
if grantType == params.Operation {
grantOk = true
break
}
}
if !grantOk {
err := fmt.Errorf("Unknown grant type")
return res, err
}
var accountDescr *descriptor.Account
var accountExists bool
switch {
case params.AccountID != 0:
accountExists, accountDescr, err = lg.db.GetAccountByID(ctx, params.AccountID)
if err != nil {
return res, err
}
case params.Username != "":
accountExists, accountDescr, err = lg.db.GetAccountByUsername(ctx, params.Username)
if err != nil {
return res, err
}
}
if !accountExists || accountDescr == nil {
err := fmt.Errorf("Account with this id or name dont exists")
return res, err
}
grantExists, _, err = lg.db.GetGrant(ctx, accountDescr.ID, params.Operation)
if err != nil {
return res, err
}
if !grantExists {
err := fmt.Errorf("Requested grant for the user not found")
return res, err
}
err = lg.db.DeleteGrantByAccountID(ctx, accountDescr.ID, params.Operation)
if err != nil {
return res, err
}
return res, err
}
+15
View File
@@ -0,0 +1,15 @@
package logic
import (
"context"
"mbase/pkg/mbctl"
)
func (lg *Logic) GetHello(ctx context.Context, params *mbctl.GetHelloParams) (*mbctl.GetHelloResult, error) {
var err error
res := &mbctl.GetHelloResult{
Message: "hello",
}
return res, err
}
+63
View File
@@ -0,0 +1,63 @@
package logic
import (
"sync/atomic"
"time"
"mbase/app/database"
"mbase/pkg/logger"
)
type LogicConfig struct {
Database *database.Database
}
type Logic struct {
log *logger.Logger
db *database.Database
dumpingSem atomic.Bool
restoringSem atomic.Bool
}
func NewLogic(conf *LogicConfig) (*Logic, error) {
var err error
lg := &Logic{
db: conf.Database,
}
lg.log = logger.NewLogger("logic")
return lg, err
}
func (lg *Logic) DumpingSemUp() {
lg.dumpingSem.Store(true)
}
func (lg *Logic) DumpingSemDown() {
lg.dumpingSem.Store(false)
}
func (lg *Logic) WaitDumping() {
for {
if !lg.dumpingSem.Load() {
return
}
time.Sleep(1 * time.Millisecond)
}
}
func (lg *Logic) RestoringSemUp() {
lg.restoringSem.Store(true)
}
func (lg *Logic) RestoringSemDown() {
lg.restoringSem.Store(false)
}
func (lg *Logic) WaitRestoring() {
for {
if !lg.restoringSem.Load() {
return
}
time.Sleep(1 * time.Millisecond)
}
}
+360
View File
@@ -0,0 +1,360 @@
package server
import (
"io/ioutil"
"os"
"os/signal"
"os/user"
"path/filepath"
"strconv"
"syscall"
"time"
"mbase/app/config"
"mbase/app/database"
"mbase/app/descriptor"
"mbase/app/logic"
"mbase/pkg/aux509"
"mbase/pkg/logger"
"mbase/pkg/netacl"
handler "mbase/app/handler"
service "mbase/app/service"
"sigs.k8s.io/yaml"
)
type Server struct {
conf *config.Config
lg *logic.Logic
svc *service.Service
hand *handler.Handler
log *logger.Logger
nacl *netacl.NetACL
db *database.Database
x509cert []byte
x509key []byte
state descriptor.Server
sfile string
}
func NewServer() (*Server, error) {
var err error
srv := &Server{}
srv.log = logger.NewLogger("server")
return srv, err
}
func (srv *Server) Configure() error {
var err error
srv.conf = config.NewConfig()
err = srv.conf.ReadFile()
if err != nil {
return err
}
err = srv.conf.ReadEnv()
if err != nil {
return err
}
err = srv.conf.ReadOpts()
if err != nil {
return err
}
srv.sfile = filepath.Join(srv.conf.DataDir, "certmanager.yaml")
return err
}
func (srv *Server) LoadState() error {
var err error
_, err = os.Stat(srv.sfile)
if os.IsNotExist(err) {
err = nil
return err
}
file, err := os.Open(srv.sfile)
if err != nil {
return err
}
defer file.Close()
stateBytes, err := ioutil.ReadAll(file)
if err != nil {
return err
}
err = yaml.Unmarshal(stateBytes, &srv.state)
if err != nil {
return err
}
return err
}
func (srv *Server) SaveState() error {
var err error
file, err := os.OpenFile(srv.sfile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0640)
if err != nil {
return err
}
defer file.Close()
srv.state.UpdatedAt = time.Now().Format(time.RFC3339)
if srv.state.CreatedAt == "" {
srv.state.CreatedAt = srv.state.UpdatedAt
}
stateBytes, err := yaml.Marshal(srv.state)
if err != nil {
return err
}
_, err = file.Write(stateBytes)
if err != nil {
return err
}
return err
}
func (srv *Server) Build() error {
var err error
srv.log.Infof("Build server")
// Mkdir log and data dir
srv.log.Infof("Create %s dir", srv.conf.DataDir)
err = os.MkdirAll(srv.conf.DataDir, 0750)
if err != nil {
return err
}
if srv.conf.Daemon {
logDir := filepath.Dir(srv.conf.LogPath)
srv.log.Infof("Create %s dir", logDir)
err = os.MkdirAll(logDir, 0750)
if err != nil {
return err
}
runDir := filepath.Dir(srv.conf.RunPath)
srv.log.Infof("Create %s dir", runDir)
err = os.MkdirAll(runDir, 0750)
if err != nil {
return err
}
}
// Create X509 certs
srv.x509cert, srv.x509key, err = aux509.CreateX509SelfSignedCert(srv.conf.Hostname)
if err != nil {
return err
}
// Load state
err = srv.LoadState()
if err != nil {
return err
}
// Create netACL
err = srv.nacl.AddDisabledAddresses(srv.conf.Networks.Disabled...)
if err != nil {
return err
}
srv.nacl = netacl.NewNetACL()
err = srv.nacl.AddEnabledAddresses(srv.conf.Networks.Enabled...)
if err != nil {
return err
}
naclYAML, err := yaml.Marshal(srv.nacl)
if err != nil {
return err
}
srv.log.Infof("Network ACL is:\n%s\n", string(naclYAML))
// Create database
srv.db, err = database.NewDatabase(srv.conf.DataDir)
err = srv.db.OpenDatabase()
if err != nil {
return err
}
// Load state
err = srv.LoadState()
if err != nil {
return err
}
// Create logic
logicConfig := &logic.LogicConfig{
Database: srv.db,
}
srv.lg, err = logic.NewLogic(logicConfig)
if err != nil {
return err
}
if !srv.state.DatabaseInitialized {
// Create schema
srv.log.Infof("Init database")
err = srv.db.InitDatabase()
if err != nil {
return err
}
srv.state.DatabaseInitialized = true
err = srv.SaveState()
if err != nil {
return err
}
}
// Create handler
handlerConfig := &handler.HandlerConfig{
Logic: srv.lg,
}
srv.hand = handler.NewHandler(handlerConfig)
if err != nil {
return err
}
// Create service
serviceConfig := &service.ServiceConfig{
Portnum: srv.conf.Service.Portnum,
Address: srv.conf.Service.Address,
Protocol: srv.conf.Service.Protocol,
Hostname: srv.conf.Hostname,
Handler: srv.hand,
Logic: srv.lg,
X509Cert: srv.x509cert,
X509Key: srv.x509key,
NetACL: srv.nacl,
}
srv.svc = service.NewService(serviceConfig)
if err != nil {
return err
}
return err
}
func (srv *Server) Run() error {
var err error
// Log configuration
yamlConfig, err := srv.conf.String()
if err != nil {
return err
}
srv.log.Debugf("Server configuration:\n%s\n", yamlConfig)
// Show current user
currUser, err := user.Current()
if err != nil {
return err
}
srv.log.Infof("Running server as user %s", currUser.Username)
sigs := make(chan os.Signal, 1)
done := make(chan error, 1)
// Run service
startService := func(svc *service.Service, done chan error) {
err = svc.Run()
if err != nil {
srv.log.Errorf("Service error: %v", err)
done <- err
}
}
go startService(srv.svc, done)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
var signal os.Signal
select {
case signal = <-sigs:
srv.log.Infof("Services stopped by signal: %v", signal)
srv.svc.Stop()
}
return err
}
func (srv *Server) PseudoFork() error {
const successExit int = 0
var keyEnv string = "IMX0LTSELMRF8KASWER"
var err error
_, isChild := os.LookupEnv(keyEnv)
switch {
case !isChild:
os.Setenv(keyEnv, "TRUE")
procAttr := syscall.ProcAttr{}
cwd, err := os.Getwd()
if err != nil {
return err
}
var sysFiles = make([]uintptr, 3)
sysFiles[0] = uintptr(syscall.Stdin)
sysFiles[1] = uintptr(syscall.Stdout)
sysFiles[2] = uintptr(syscall.Stderr)
procAttr.Files = sysFiles
procAttr.Env = os.Environ()
procAttr.Dir = cwd
_, err = syscall.ForkExec(os.Args[0], os.Args, &procAttr)
if err != nil {
return err
}
os.Exit(successExit)
case isChild:
_, err = syscall.Setsid()
if err != nil {
return err
}
}
os.Unsetenv(keyEnv)
return err
}
func (srv *Server) Daemonize() error {
var err error
if srv.conf.Daemon {
// Restart process process
err = srv.PseudoFork()
if err != nil {
return err
}
// Redirect stdin
nullFile, err := os.OpenFile("/dev/null", os.O_RDWR, 0)
if err != nil {
return err
}
err = syscall.Dup2(int(nullFile.Fd()), int(os.Stdin.Fd()))
if err != nil {
return err
}
// Redirect stderr and stout
logdir := filepath.Dir(srv.conf.LogPath)
err = os.MkdirAll(logdir, 0750)
if err != nil {
return err
}
logFile, err := os.OpenFile(srv.conf.LogPath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0640)
if err != nil {
return err
}
err = syscall.Dup2(int(logFile.Fd()), int(os.Stdout.Fd()))
if err != nil {
return err
}
err = syscall.Dup2(int(logFile.Fd()), int(os.Stderr.Fd()))
if err != nil {
return err
}
// Write process ID
rundir := filepath.Dir(srv.conf.RunPath)
err = os.MkdirAll(rundir, 0750)
if err != nil {
return err
}
pidFile, err := os.OpenFile(srv.conf.RunPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0640)
if err != nil {
return err
}
defer pidFile.Close()
currPid := os.Getpid()
_, err = pidFile.WriteString(strconv.Itoa(currPid))
if err != nil {
return err
}
}
return err
}
+151
View File
@@ -0,0 +1,151 @@
package service
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"net"
"mbase/app/handler"
"mbase/app/logic"
"mbase/pkg/logger"
"mbase/pkg/netacl"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/peer"
)
type ServiceConfig struct {
Handler *handler.Handler
Logic *logic.Logic
NetACL *netacl.NetACL
Portnum uint32
Address string
Protocol string
Hostname string
X509Cert []byte
X509Key []byte
}
type Service struct {
gsrv *grpc.Server
hand *handler.Handler
lg *logic.Logic
log *logger.Logger
nacl *netacl.NetACL
portnum uint32
address string
protocol string
hostname string
username string
password string
x509Cert []byte
x509Key []byte
}
func NewService(conf *ServiceConfig) *Service {
svc := Service{
hand: conf.Handler,
lg: conf.Logic,
nacl: conf.NetACL,
portnum: conf.Portnum,
address: conf.Address,
protocol: conf.Protocol,
hostname: conf.Hostname,
x509Cert: conf.X509Cert,
x509Key: conf.X509Key,
}
svc.log = logger.NewLogger("gservice")
return &svc
}
func (svc *Service) Run() error {
var err error
svc.log.Infof("Service run")
listenSpec := fmt.Sprintf("%s:%d", svc.address, svc.portnum)
listener, err := net.Listen(svc.protocol, listenSpec)
if err != nil {
return err
}
tlsCert, err := tls.X509KeyPair(svc.x509Cert, svc.x509Key)
if err != nil {
return err
}
tlsConfig := tls.Config{
Certificates: []tls.Certificate{tlsCert},
ClientAuth: tls.NoClientCert,
InsecureSkipVerify: true,
}
tlsCredentials := credentials.NewTLS(&tlsConfig)
if err != nil {
return err
}
interceptors := []grpc.UnaryServerInterceptor{
svc.accessInterceptor,
svc.logInterceptor,
}
gsrvOpts := []grpc.ServerOption{
grpc.Creds(tlsCredentials),
grpc.ChainUnaryInterceptor(interceptors...),
}
svc.gsrv = grpc.NewServer(gsrvOpts...)
svc.hand.Register(svc.gsrv)
svc.log.Infof("Service listening at %v", listener.Addr())
err = svc.gsrv.Serve(listener)
if err != nil {
return err
}
return err
}
func (svc *Service) accessInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
peerMeta, _ := peer.FromContext(ctx)
host, _, err := net.SplitHostPort(peerMeta.Addr.String())
if err != nil {
return nil, err
}
addressEnabled, _ := svc.nacl.AddressIsEnabled(host)
if err != nil {
return nil, err
}
if addressEnabled {
svc.log.Warningf("Enable access from %s", host)
return handler(ctx, req)
}
svc.log.Warningf("Disable access from %s", host)
return nil, fmt.Errorf("Access disabled by network ACL")
}
func (svc *Service) logInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
meta, _ := metadata.FromIncomingContext(ctx)
peerMeta, _ := peer.FromContext(ctx)
svc.log.Infof("User %v called %v from %s", meta["username"], info.FullMethod, peerMeta.Addr.String())
return handler(ctx, req)
}
func (svc *Service) debugInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
var err error
reqBinary, err := json.Marshal(req)
requestString := ""
if err == nil {
requestString = string(reqBinary)
}
svc.log.Debugf("Called method: %v with params %v", info.FullMethod, requestString)
return handler(ctx, req)
}
func (svc *Service) Stop() {
svc.log.Infof("Stopping service")
svc.gsrv.GracefulStop()
}