diff --git a/Makefile b/Makefile index 6c1bccd..e12fbec 100644 --- a/Makefile +++ b/Makefile @@ -5,4 +5,10 @@ build-cli: go build -o bin/wdbctl ./cmd/wdbctl/cli.go run: - go run ./cmd/wunderdb/wdb.go \ No newline at end of file + go run ./cmd/wunderdb/wdb.go + +gen-txlog-models: + gojsonschema -p txlModel internal/txlogs/model/txlog.schema.json -o internal/txlogs/model/model.go + +instal-dev: + go get github.com/atombender/go-jsonschema/... \ No newline at end of file diff --git a/go.mod b/go.mod index bfb1ca7..6725518 100644 --- a/go.mod +++ b/go.mod @@ -36,8 +36,8 @@ require ( github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect - golang.org/x/crypto v0.0.0-20220214200702-86341886e292 // indirect - golang.org/x/sys v0.3.0 // indirect - golang.org/x/text v0.3.8 // indirect + golang.org/x/crypto v0.7.0 // indirect + golang.org/x/sys v0.6.0 // indirect + golang.org/x/text v0.8.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index d8a73aa..bbcafc8 100644 --- a/go.sum +++ b/go.sum @@ -91,6 +91,8 @@ github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsr golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220214200702-86341886e292 h1:f+lwQ+GtmgoY+A2YaQxlSOnDjXcQ7ZRLWOHbC6HtRqE= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220906165146-f3363e06e74c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -103,12 +105,16 @@ golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/config/config.go b/internal/config/config.go index 14dd39b..1d9bcda 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -26,6 +26,7 @@ const ( ) type Config struct { + RootDirectoryPath string `json:"ROOT_DIR_PATH"` AdminID string `json:"ADMIN_ID"` AdminPassword string `json:"-"` Port string `json:"PORT"` @@ -69,6 +70,7 @@ func Load() (*Config, error) { } c := &Config{ + RootDirectoryPath: wdbRootDirectory, AdminID: configMap.getValue(ADMIN_ID, override), AdminPassword: configMap.getValue(ADMIN_PASSWORD, override), Port: configMap.getValue(PORT, override), diff --git a/internal/config/constant.go b/internal/config/constant.go index 569b0a4..2a27d78 100644 --- a/internal/config/constant.go +++ b/internal/config/constant.go @@ -8,6 +8,7 @@ const ( PORT = "PORT" PERSISTANT_STORAGE_PATH = "PERSISTANT_STORAGE_PATH" OVERRIDE_CONFIG = "OVERRIDE_CONFIG" + ROOT_DIR_PATH = "ROOT_DIR_PATH" ) var defaultValues = map[string]string{ diff --git a/internal/privileges/constants.go b/internal/privileges/constants.go index 5e218e9..3b1c9c4 100644 --- a/internal/privileges/constants.go +++ b/internal/privileges/constants.go @@ -1,5 +1,9 @@ package privileges +// Scope of Privilege Type +// TODO: Use for below Privileges (ref. L14-21) +type PrivilegeScopeType string + const ( UserPrivileges = "userPrivilege" GlobalPrivileges = "globalPrivilege" @@ -7,6 +11,20 @@ const ( CollectionPrivileges = "collectionPrivilege" ) +// Read, Write, Wildcard Action Type +type PrivilegeActionType string + +var ( + WildcardPrivilege PrivilegeActionType = "wildcardPrivilege" + WritePrivilege PrivilegeActionType = "writePrivilege" + ReadPrivilege PrivilegeActionType = "readPrivilege" +) + +const ( + Allowed = true + Denied = false +) + const ( Wildcard = "*" ) @@ -37,8 +55,3 @@ const ( UpdateData = "updateData" DeleteData = "deleteData" ) - -const ( - Allowed = true - Denied = false -) diff --git a/internal/privileges/privileges.go b/internal/privileges/privileges.go index 5d6bc3b..cba5c4b 100644 --- a/internal/privileges/privileges.go +++ b/internal/privileges/privileges.go @@ -1,26 +1,53 @@ package privileges -var ( - PrivilegeScope = map[string]string{ - CreateRole: GlobalPrivileges, - CreateDatabase: GlobalPrivileges, - LoginUser: GlobalPrivileges, - ListRole: GlobalPrivileges, - GrantRole: UserPrivileges, - UpdateRole: UserPrivileges, - ReadDatabase: DatabasePrivileges, - UpdateDatabase: DatabasePrivileges, - DeleteDatabase: DatabasePrivileges, - CreateCollection: DatabasePrivileges, - ReadCollection: CollectionPrivileges, - UpdateCollection: CollectionPrivileges, - DeleteCollection: CollectionPrivileges, - AddData: CollectionPrivileges, - ReadData: CollectionPrivileges, - UpdateData: CollectionPrivileges, - DeleteData: CollectionPrivileges, - } -) +// TODO: merge PrivilegeScope and PrivilegeType maps +// into on map/struct and use same everywhere, eg: +// +// map[string]struct{ +// Scope PrivilegeScopeType +// Type PrivilegeActionType +// } +var PrivilegeScope = map[string]string{ + CreateRole: GlobalPrivileges, + CreateDatabase: GlobalPrivileges, + LoginUser: GlobalPrivileges, + ListRole: GlobalPrivileges, + GrantRole: UserPrivileges, + UpdateRole: UserPrivileges, + ReadDatabase: DatabasePrivileges, + UpdateDatabase: DatabasePrivileges, + DeleteDatabase: DatabasePrivileges, + CreateCollection: DatabasePrivileges, + ReadCollection: CollectionPrivileges, + UpdateCollection: CollectionPrivileges, + DeleteCollection: CollectionPrivileges, + AddData: CollectionPrivileges, + ReadData: CollectionPrivileges, + UpdateData: CollectionPrivileges, + DeleteData: CollectionPrivileges, +} + +var PrivilegeType = map[string]PrivilegeActionType{ + CreateRole: WildcardPrivilege, + LoginUser: WildcardPrivilege, + GrantRole: WildcardPrivilege, + UpdateRole: WildcardPrivilege, + ListRole: WildcardPrivilege, + + ReadDatabase: ReadPrivilege, + ReadCollection: ReadPrivilege, + ReadData: ReadPrivilege, + + CreateDatabase: WritePrivilege, + UpdateDatabase: WritePrivilege, + DeleteDatabase: WritePrivilege, + CreateCollection: WritePrivilege, + UpdateCollection: WritePrivilege, + DeleteCollection: WritePrivilege, + AddData: WritePrivilege, + UpdateData: WritePrivilege, + DeleteData: WritePrivilege, +} func IsAvailable(privilege string) bool { _, privilegeExists := PrivilegeScope[privilege] @@ -33,3 +60,11 @@ func Category(privilege string) string { } return "" } + +func GetPrivilegeType(privilege string) PrivilegeActionType { + privilegeType, ok := PrivilegeType[privilege] + if ok { + return privilegeType + } + return WildcardPrivilege +} diff --git a/internal/server/handlers/collection.go b/internal/server/handlers/collection.go index 036d0d6..4f71e1a 100644 --- a/internal/server/handlers/collection.go +++ b/internal/server/handlers/collection.go @@ -26,10 +26,11 @@ func (wh wdbHandlers) CreateCollection(c *fiber.Ctx) error { } entities := model.Entities{ - Databases: &databaseName, + Databases: &databaseName, + Collections: &collection.Name, // check } - if err := ValidateRequest(collection); err != nil { + if err := validateRequest(collection); err != nil { apiError = err } else { isValid, error := wh.handleAuthenticationAndAuthorization(c, entities, privilege) @@ -42,7 +43,15 @@ func (wh wdbHandlers) CreateCollection(c *fiber.Ctx) error { resp := response.Format(privilege, apiError, nil) - return SendResponse(c, resp.Marshal(), resp.HttpStatusCode) + if err := sendResponse(c, resp); err != nil { + return err + } + + if err := wh.handleTransactions(c, resp, entities); err != nil { + return err + } + + return nil } func (wh wdbHandlers) FetchCollection(c *fiber.Ctx) error { @@ -68,7 +77,15 @@ func (wh wdbHandlers) FetchCollection(c *fiber.Ctx) error { resp := response.Format(privilege, apiError, fetchedDatabase) - return SendResponse(c, resp.Marshal(), resp.HttpStatusCode) + if err := sendResponse(c, resp); err != nil { + return err + } + + if err := wh.handleTransactions(c, resp, entities); err != nil { + return err + } + + return nil } func (wh wdbHandlers) DeleteCollection(c *fiber.Ctx) error { @@ -93,5 +110,13 @@ func (wh wdbHandlers) DeleteCollection(c *fiber.Ctx) error { resp := response.Format(privilege, apiError, nil) - return SendResponse(c, resp.Marshal(), resp.HttpStatusCode) + if err := sendResponse(c, resp); err != nil { + return err + } + + if err := wh.handleTransactions(c, resp, entities); err != nil { + return err + } + + return nil } diff --git a/internal/server/handlers/common.go b/internal/server/handlers/common.go index 34824c5..cf7e58a 100644 --- a/internal/server/handlers/common.go +++ b/internal/server/handlers/common.go @@ -1,6 +1,9 @@ package handlers import ( + "github.com/TanmoySG/wunderDB/internal/server/response" + "github.com/TanmoySG/wunderDB/internal/txlogs" + txlModel "github.com/TanmoySG/wunderDB/internal/txlogs/model" "github.com/TanmoySG/wunderDB/internal/users/authentication" "github.com/TanmoySG/wunderDB/model" er "github.com/TanmoySG/wunderDB/pkg/wdb/errors" @@ -8,15 +11,17 @@ import ( "github.com/gofiber/fiber/v2" ) -var ( - noEntities = model.Entities{} -) - const ( + AuthorizationHeader = "Authorization" + authSuccessful = true authFailure = false ) +var ( + noEntities = model.Entities{} +) + func (wh wdbHandlers) handleAuthenticationAndAuthorization(c *fiber.Ctx, entities model.Entities, privilege string) (bool, *er.WdbError) { username, isValidUser, error := wh.handleAuthentication(c) @@ -65,13 +70,24 @@ func (wh wdbHandlers) handleAuthorization(username string, entity model.Entities return authSuccessful, nil } -func SendResponse(c *fiber.Ctx, marshaledResponse []byte, statusCode int) error { +func sendResponse(c *fiber.Ctx, apiResponse response.ApiResponse) error { c.Set(ContentType, ApplicationJson) - c.Send(marshaledResponse) - return c.SendStatus(statusCode) + + marshaledResponse := apiResponse.Marshal() + err := c.Send(marshaledResponse) + if err != nil { + return err + } + + err = c.SendStatus(apiResponse.HttpStatusCode) + if err != nil { + return err + } + + return nil } -func ValidateRequest(request any) *er.WdbError { +func validateRequest(request any) *er.WdbError { validate := validator.New() err := validate.Struct(request) @@ -80,3 +96,35 @@ func ValidateRequest(request any) *er.WdbError { } return nil } + +func (wh wdbHandlers) handleTransactions(c *fiber.Ctx, apiResponse response.ApiResponse, entities model.Entities) error { + if txlogs.IsTxnLoggable(apiResponse.Response.Action) { + if apiResponse.Response.Status == response.StatusSuccess { + databaseEntity := *entities.Databases + if entities.Databases == nil { + databaseEntity = "" + } + + txnActor := txlogs.GetTxnActor(c.Get(AuthorizationHeader)) + txnAction := apiResponse.Response.Action + + txnHttpDetails := txlogs.GetTxnHttpDetails(*c) + txnEntityPath := txlModel.TxlogSchemaJsonEntityPath{ + Database: databaseEntity, + Collection: entities.Collections, + } + + txnLog := txlogs.CreateTxLog(txnAction, txnActor, apiResponse.Response.Status, txnEntityPath, txnHttpDetails) + + err := wh.wdbTxLogs.Log(txnLog) + if err != nil { + return err + } + err = wh.wdbTxLogs.Commit() + if err != nil { + return err + } + } + } + return nil +} diff --git a/internal/server/handlers/data.go b/internal/server/handlers/data.go index e5a64f6..6d820e2 100644 --- a/internal/server/handlers/data.go +++ b/internal/server/handlers/data.go @@ -39,7 +39,15 @@ func (wh wdbHandlers) AddData(c *fiber.Ctx) error { resp := response.Format(privilege, apiError, nil) - return SendResponse(c, resp.Marshal(), resp.HttpStatusCode) + if err := sendResponse(c, resp); err != nil { + return err + } + + if err := wh.handleTransactions(c, resp, entities); err != nil { + return err + } + + return nil } func (wh wdbHandlers) ReadData(c *fiber.Ctx) error { @@ -76,7 +84,15 @@ func (wh wdbHandlers) ReadData(c *fiber.Ctx) error { resp := response.Format(privilege, apiError, fetchedData) - return SendResponse(c, resp.Marshal(), resp.HttpStatusCode) + if err := sendResponse(c, resp); err != nil { + return err + } + + if err := wh.handleTransactions(c, resp, entities); err != nil { + return err + } + + return nil } func (wh wdbHandlers) DeleteData(c *fiber.Ctx) error { @@ -113,7 +129,15 @@ func (wh wdbHandlers) DeleteData(c *fiber.Ctx) error { resp := response.Format(privilege, apiError, fetchedData) - return SendResponse(c, resp.Marshal(), resp.HttpStatusCode) + if err := sendResponse(c, resp); err != nil { + return err + } + + if err := wh.handleTransactions(c, resp, entities); err != nil { + return err + } + + return nil } func (wh wdbHandlers) UpdateData(c *fiber.Ctx) error { @@ -155,5 +179,13 @@ func (wh wdbHandlers) UpdateData(c *fiber.Ctx) error { resp := response.Format(privilege, apiError, fetchedData) - return SendResponse(c, resp.Marshal(), resp.HttpStatusCode) + if err := sendResponse(c, resp); err != nil { + return err + } + + if err := wh.handleTransactions(c, resp, entities); err != nil { + return err + } + + return nil } diff --git a/internal/server/handlers/database.go b/internal/server/handlers/database.go index 746a7bb..157a6b5 100644 --- a/internal/server/handlers/database.go +++ b/internal/server/handlers/database.go @@ -14,7 +14,6 @@ type database struct { func (wh wdbHandlers) CreateDatabase(c *fiber.Ctx) error { privilege := privileges.CreateDatabase - var apiError *er.WdbError database := new(database) @@ -22,7 +21,11 @@ func (wh wdbHandlers) CreateDatabase(c *fiber.Ctx) error { return err } - if err := ValidateRequest(database); err != nil { + entities := model.Entities{ + Databases: &database.Name, + } + + if err := validateRequest(database); err != nil { apiError = err } else { isValid, error := wh.handleAuthenticationAndAuthorization(c, noEntities, privilege) @@ -34,7 +37,15 @@ func (wh wdbHandlers) CreateDatabase(c *fiber.Ctx) error { } resp := response.Format(privilege, apiError, nil) - return SendResponse(c, resp.Marshal(), resp.HttpStatusCode) + if err := sendResponse(c, resp); err != nil { + return err + } + + if err := wh.handleTransactions(c, resp, entities); err != nil { + return err + } + + return nil } func (wh wdbHandlers) FetchDatabase(c *fiber.Ctx) error { @@ -57,7 +68,15 @@ func (wh wdbHandlers) FetchDatabase(c *fiber.Ctx) error { resp := response.Format(privilege, apiError, fetchedDatabase) - return SendResponse(c, resp.Marshal(), resp.HttpStatusCode) + if err := sendResponse(c, resp); err != nil { + return err + } + + if err := wh.handleTransactions(c, resp, entities); err != nil { + return err + } + + return nil } func (wh wdbHandlers) DeleteDatabase(c *fiber.Ctx) error { @@ -79,5 +98,13 @@ func (wh wdbHandlers) DeleteDatabase(c *fiber.Ctx) error { resp := response.Format(privilege, apiError, nil) - return SendResponse(c, resp.Marshal(), resp.HttpStatusCode) + if err := sendResponse(c, resp); err != nil { + return err + } + + if err := wh.handleTransactions(c, resp, entities); err != nil { + return err + } + + return nil } diff --git a/internal/server/handlers/handlers.go b/internal/server/handlers/handlers.go index 93ae8f3..a88e00f 100644 --- a/internal/server/handlers/handlers.go +++ b/internal/server/handlers/handlers.go @@ -1,12 +1,21 @@ package handlers import ( + tx "github.com/TanmoySG/wunderDB/internal/txlogs" w "github.com/TanmoySG/wunderDB/pkg/wdb" "github.com/gofiber/fiber/v2" ) type wdbHandlers struct { wdbClient w.Client + wdbTxLogs tx.DotTxLog +} + +func NewHandlers(client w.Client, wdbBasePath string) Client { + return wdbHandlers{ + wdbClient: client, + wdbTxLogs: tx.UseDotTxLog(wdbBasePath), + } } type Client interface { @@ -39,9 +48,3 @@ type Client interface { // CheckPermissions(c *fiber.Ctx) error } - -func NewHandlers(client w.Client) Client { - return wdbHandlers{ - wdbClient: client, - } -} diff --git a/internal/server/handlers/hello.go b/internal/server/handlers/hello.go index 1d3a0a8..d2f8503 100644 --- a/internal/server/handlers/hello.go +++ b/internal/server/handlers/hello.go @@ -21,5 +21,13 @@ func (wh wdbHandlers) Hello(c *fiber.Ctx) error { } resp := response.Format("ping", nil, msg) - return SendResponse(c, resp.Marshal(), resp.HttpStatusCode) + if err := sendResponse(c, resp); err != nil { + return err + } + + if err := wh.handleTransactions(c, resp, noEntities); err != nil { + return err + } + + return nil } diff --git a/internal/server/handlers/roles.go b/internal/server/handlers/roles.go index 57e95f1..cf89b1b 100644 --- a/internal/server/handlers/roles.go +++ b/internal/server/handlers/roles.go @@ -25,7 +25,7 @@ func (wh wdbHandlers) CreateRole(c *fiber.Ctx) error { return err } - if err := ValidateRequest(r); err != nil { + if err := validateRequest(r); err != nil { apiError = err } else { isValid, error := wh.handleAuthenticationAndAuthorization(c, noEntities, privilege) @@ -35,9 +35,18 @@ func (wh wdbHandlers) CreateRole(c *fiber.Ctx) error { apiError = wh.wdbClient.CreateRole(model.Identifier(r.Role), r.Allowed, r.Denied) } } + resp := response.Format(privilege, apiError, nil) - return SendResponse(c, resp.Marshal(), resp.HttpStatusCode) + if err := sendResponse(c, resp); err != nil { + return err + } + + if err := wh.handleTransactions(c, resp, noEntities); err != nil { + return err + } + + return nil } func (wh wdbHandlers) ListRoles(c *fiber.Ctx) error { @@ -55,5 +64,13 @@ func (wh wdbHandlers) ListRoles(c *fiber.Ctx) error { resp := response.Format(privilege, apiError, roleList) - return SendResponse(c, resp.Marshal(), resp.HttpStatusCode) + if err := sendResponse(c, resp); err != nil { + return err + } + + if err := wh.handleTransactions(c, resp, noEntities); err != nil { + return err + } + + return nil } diff --git a/internal/server/handlers/users.go b/internal/server/handlers/users.go index bc49a6c..d0ffa35 100644 --- a/internal/server/handlers/users.go +++ b/internal/server/handlers/users.go @@ -33,7 +33,15 @@ func (wh wdbHandlers) LoginUser(c *fiber.Ctx) error { resp := response.Format(privilege, apiError, data) - return SendResponse(c, resp.Marshal(), resp.HttpStatusCode) + if err := sendResponse(c, resp); err != nil { + return err + } + + if err := wh.handleTransactions(c, resp, noEntities); err != nil { + return err + } + + return nil } func (wh wdbHandlers) CreateUser(c *fiber.Ctx) error { @@ -45,19 +53,29 @@ func (wh wdbHandlers) CreateUser(c *fiber.Ctx) error { return err } - if err := ValidateRequest(u); err != nil { + if err := validateRequest(u); err != nil { apiError = err } else { apiError = wh.wdbClient.CreateUser(model.Identifier(u.Username), u.Password) } + resp := response.Format(privilege, apiError, nil) - return SendResponse(c, resp.Marshal(), resp.HttpStatusCode) + if err := sendResponse(c, resp); err != nil { + return err + } + + if err := wh.handleTransactions(c, resp, noEntities); err != nil { + return err + } + + return nil } func (wh wdbHandlers) GrantRoles(c *fiber.Ctx) error { privilege := privileges.GrantRole + var entities model.Entities var data map[string]interface{} var apiError *er.WdbError @@ -67,11 +85,9 @@ func (wh wdbHandlers) GrantRoles(c *fiber.Ctx) error { return err } - if err := ValidateRequest(up); err != nil { + if err := validateRequest(up); err != nil { apiError = err } else { - var entities model.Entities - if up.Permission.On != nil { entities = model.Entities{ Databases: up.Permission.On.Databases, @@ -88,7 +104,15 @@ func (wh wdbHandlers) GrantRoles(c *fiber.Ctx) error { } resp := response.Format(privilege, apiError, data) - return SendResponse(c, resp.Marshal(), resp.HttpStatusCode) + if err := sendResponse(c, resp); err != nil { + return err + } + + if err := wh.handleTransactions(c, resp, noEntities); err != nil { + return err + } + + return nil } func (wh wdbHandlers) CheckPermissions(c *fiber.Ctx) error { @@ -108,5 +132,13 @@ func (wh wdbHandlers) CheckPermissions(c *fiber.Ctx) error { } resp := response.Format("", error, data) - return SendResponse(c, resp.Marshal(), resp.HttpStatusCode) + if err := sendResponse(c, resp); err != nil { + return err + } + + if err := wh.handleTransactions(c, resp, entities); err != nil { + return err + } + + return nil } diff --git a/internal/server/lifecycle/shutdown/shudown.go b/internal/server/lifecycle/shutdown/shutdown.go similarity index 100% rename from internal/server/lifecycle/shutdown/shudown.go rename to internal/server/lifecycle/shutdown/shutdown.go diff --git a/internal/server/lifecycle/startup/startup.go b/internal/server/lifecycle/startup/startup.go index ae849c7..88c4406 100644 --- a/internal/server/lifecycle/startup/startup.go +++ b/internal/server/lifecycle/startup/startup.go @@ -55,6 +55,6 @@ func Start(w *model.WDB, c *config.Config) { wdbc := wdbClient.NewWdbClient(wdbClientConfigurations, w.Databases, w.Roles, w.Users, authentication.MD5) wdbc.InitializeAdmin(c) - server := wdbs.NewWdbServer(wdbc, c.Port) + server := wdbs.NewWdbServer(wdbc, c.RootDirectoryPath, c.Port) server.Start() } diff --git a/internal/server/server.go b/internal/server/server.go index baf0006..9bda8c1 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -25,10 +25,10 @@ type Client interface { Start() } -func NewWdbServer(wdbClient wdbClient.Client, port string) Client { +func NewWdbServer(wdbClient wdbClient.Client, wdbBasePath, port string) Client { return wdbServer{ port: fmt.Sprintf(":%s", port), - handler: handlers.NewHandlers(wdbClient), + handler: handlers.NewHandlers(wdbClient, wdbBasePath), } } @@ -42,6 +42,7 @@ func (ws wdbServer) Start() { recoveryConf := recovery.DefaultConfig recoveryConf.Message = &defaultPanicMessage + app.Use(logger.New()) app.Use(recovery.New(recoveryConf)) diff --git a/internal/txlogs/constants.go b/internal/txlogs/constants.go new file mode 100644 index 0000000..2b57cc7 --- /dev/null +++ b/internal/txlogs/constants.go @@ -0,0 +1,6 @@ +package txlogs + +var WDB_DOT_TX_LOG_BASEPATH = "%s/txlogs" +var WDB_DOT_TX_LOG_FILEPATH = "%s/%s" + +const wdbDotTxLogFilename = "tx.log.json" diff --git a/internal/txlogs/dotTxLogs.go b/internal/txlogs/dotTxLogs.go new file mode 100644 index 0000000..c2695df --- /dev/null +++ b/internal/txlogs/dotTxLogs.go @@ -0,0 +1,67 @@ +package txlogs + +import ( + "encoding/json" + "fmt" + + txlModel "github.com/TanmoySG/wunderDB/internal/txlogs/model" + "github.com/TanmoySG/wunderDB/pkg/fs" +) + +type DotTxLog struct { + txLogFilepath string + transactionLogs TransactionLogs +} + +// return error too : if createTxLogBase returns error +// UseDotTxLog should also return error and fail +func UseDotTxLog(wunderDbBasePath string) DotTxLog { + wdbTxLogBasePath := fmt.Sprintf(WDB_DOT_TX_LOG_BASEPATH, wunderDbBasePath) + createTxLogBase(wdbTxLogBasePath) + + return DotTxLog{ + txLogFilepath: fmt.Sprintf(WDB_DOT_TX_LOG_FILEPATH, wdbTxLogBasePath, wdbDotTxLogFilename), + } +} + +func (dotTxL *DotTxLog) Commit() error { + preCommitTxLogBytes, err := dotTxL.transactionLogs.Marshal() + if err != nil { + return err + } + return fs.WriteToFile(dotTxL.txLogFilepath, preCommitTxLogBytes) + +} + +func (dotTxL *DotTxLog) Log(newLog txlModel.TxlogSchemaJson) error { + dotTxLFilepath := dotTxL.txLogFilepath + if !fs.CheckFileExists(dotTxLFilepath) { + err := fs.CreateFile(dotTxLFilepath) + if err != nil { + return err + } + + err = fs.WriteToFile(dotTxLFilepath, []byte("{}")) + if err != nil { + return err + } + } + + previousTxLogBytes, err := fs.ReadFile(dotTxLFilepath) + if err != nil { + return err + } + + err = json.Unmarshal(previousTxLogBytes, &dotTxL.transactionLogs) + if err != nil { + return err + } + + dotTxL.transactionLogs.Logs = append(dotTxL.transactionLogs.Logs, &newLog) + + return nil +} + +func createTxLogBase(dirPath string) { + _ = fs.CreateDirectory(dirPath) +} diff --git a/internal/txlogs/model/methods.go b/internal/txlogs/model/methods.go new file mode 100644 index 0000000..c48fece --- /dev/null +++ b/internal/txlogs/model/methods.go @@ -0,0 +1,7 @@ +package txlModel + +import "encoding/json" + +func (j *TxlogSchemaJson) Marshal() ([]byte, error) { + return json.Marshal(j) +} diff --git a/internal/txlogs/model/model.go b/internal/txlogs/model/model.go new file mode 100644 index 0000000..ab2211a --- /dev/null +++ b/internal/txlogs/model/model.go @@ -0,0 +1,287 @@ +// Code generated by github.com/atombender/go-jsonschema, DO NOT EDIT. + +package txlModel + +import "fmt" +import "encoding/json" +import "reflect" + +// UnmarshalJSON implements json.Unmarshaler. +func (j *TxlogSchemaJsonStatus) UnmarshalJSON(b []byte) error { + var v string + if err := json.Unmarshal(b, &v); err != nil { + return err + } + var ok bool + for _, expected := range enumValues_TxlogSchemaJsonStatus { + if reflect.DeepEqual(v, expected) { + ok = true + break + } + } + if !ok { + return fmt.Errorf("invalid value (expected one of %#v): %#v", enumValues_TxlogSchemaJsonStatus, v) + } + *j = TxlogSchemaJsonStatus(v) + return nil +} + +// UnmarshalJSON implements json.Unmarshaler. +func (j *TxlogSchemaJsonEntityPath) UnmarshalJSON(b []byte) error { + var raw map[string]interface{} + if err := json.Unmarshal(b, &raw); err != nil { + return err + } + if v, ok := raw["database"]; !ok || v == nil { + return fmt.Errorf("field database: required") + } + type Plain TxlogSchemaJsonEntityPath + var plain Plain + if err := json.Unmarshal(b, &plain); err != nil { + return err + } + *j = TxlogSchemaJsonEntityPath(plain) + return nil +} + +type TxlogSchemaJson struct { + // action performed on entity, pick from permissions + Action string `json:"action"` + + // actor of transaction + Actor *string `json:"actor,omitempty"` + + // action performed on entity, pick from permissions + EntityPath TxlogSchemaJsonEntityPath `json:"entity_path"` + + // type of entity + EntityType TxlogSchemaJsonEntityType `json:"entity_type"` + + // status of transaction + Status TxlogSchemaJsonStatus `json:"status"` + + // timestamp of transaction + Timestamp float64 `json:"timestamp"` + + // HTTP details of transaction + TransactionDetails TxlogSchemaJsonTransactionDetails `json:"transaction_details"` +} + +// UnmarshalJSON implements json.Unmarshaler. +func (j *TxlogSchemaJsonTransactionDetails) UnmarshalJSON(b []byte) error { + var raw map[string]interface{} + if err := json.Unmarshal(b, &raw); err != nil { + return err + } + if v, ok := raw["method"]; !ok || v == nil { + return fmt.Errorf("field method: required") + } + if v, ok := raw["request"]; !ok || v == nil { + return fmt.Errorf("field request: required") + } + if v, ok := raw["response"]; !ok || v == nil { + return fmt.Errorf("field response: required") + } + if v, ok := raw["url_endpoint"]; !ok || v == nil { + return fmt.Errorf("field url_endpoint: required") + } + type Plain TxlogSchemaJsonTransactionDetails + var plain Plain + if err := json.Unmarshal(b, &plain); err != nil { + return err + } + *j = TxlogSchemaJsonTransactionDetails(plain) + return nil +} + +// UnmarshalJSON implements json.Unmarshaler. +func (j *TxlogSchemaJsonEntityType) UnmarshalJSON(b []byte) error { + var v string + if err := json.Unmarshal(b, &v); err != nil { + return err + } + var ok bool + for _, expected := range enumValues_TxlogSchemaJsonEntityType { + if reflect.DeepEqual(v, expected) { + ok = true + break + } + } + if !ok { + return fmt.Errorf("invalid value (expected one of %#v): %#v", enumValues_TxlogSchemaJsonEntityType, v) + } + *j = TxlogSchemaJsonEntityType(v) + return nil +} + +// UnmarshalJSON implements json.Unmarshaler. +func (j *TxlogSchemaJsonTransactionDetailsResponse) UnmarshalJSON(b []byte) error { + var raw map[string]interface{} + if err := json.Unmarshal(b, &raw); err != nil { + return err + } + if v, ok := raw["http_status"]; !ok || v == nil { + return fmt.Errorf("field http_status: required") + } + if v, ok := raw["response_body"]; !ok || v == nil { + return fmt.Errorf("field response_body: required") + } + type Plain TxlogSchemaJsonTransactionDetailsResponse + var plain Plain + if err := json.Unmarshal(b, &plain); err != nil { + return err + } + *j = TxlogSchemaJsonTransactionDetailsResponse(plain) + return nil +} + +// UnmarshalJSON implements json.Unmarshaler. +func (j *TxlogSchemaJsonTransactionDetailsRequest) UnmarshalJSON(b []byte) error { + var raw map[string]interface{} + if err := json.Unmarshal(b, &raw); err != nil { + return err + } + if v, ok := raw["is_authenticated"]; !ok || v == nil { + return fmt.Errorf("field is_authenticated: required") + } + type Plain TxlogSchemaJsonTransactionDetailsRequest + var plain Plain + if err := json.Unmarshal(b, &plain); err != nil { + return err + } + *j = TxlogSchemaJsonTransactionDetailsRequest(plain) + return nil +} + +// UnmarshalJSON implements json.Unmarshaler. +func (j *TxlogSchemaJsonTransactionDetailsMethod) UnmarshalJSON(b []byte) error { + var v string + if err := json.Unmarshal(b, &v); err != nil { + return err + } + var ok bool + for _, expected := range enumValues_TxlogSchemaJsonTransactionDetailsMethod { + if reflect.DeepEqual(v, expected) { + ok = true + break + } + } + if !ok { + return fmt.Errorf("invalid value (expected one of %#v): %#v", enumValues_TxlogSchemaJsonTransactionDetailsMethod, v) + } + *j = TxlogSchemaJsonTransactionDetailsMethod(v) + return nil +} + +// action performed on entity, pick from permissions +type TxlogSchemaJsonEntityPath struct { + // collection name, must also have database + Collection *string `json:"collection,omitempty"` + + // database name + Database string `json:"database"` +} + +type TxlogSchemaJsonEntityType string + +const TxlogSchemaJsonEntityTypeCOLLECTION TxlogSchemaJsonEntityType = "COLLECTION" +const TxlogSchemaJsonEntityTypeDATABASE TxlogSchemaJsonEntityType = "DATABASE" + +type TxlogSchemaJsonStatus string + +const TxlogSchemaJsonStatusFAILED TxlogSchemaJsonStatus = "FAILED" +const TxlogSchemaJsonStatusSUCCESS TxlogSchemaJsonStatus = "SUCCESS" + +// HTTP details of transaction +type TxlogSchemaJsonTransactionDetails struct { + // HTTP method of transaction + Method TxlogSchemaJsonTransactionDetailsMethod `json:"method"` + + // request details of transaction + Request TxlogSchemaJsonTransactionDetailsRequest `json:"request"` + + // response details of transaction + Response TxlogSchemaJsonTransactionDetailsResponse `json:"response"` + + // transaction endpoint/url + UrlEndpoint string `json:"url_endpoint"` +} + +type TxlogSchemaJsonTransactionDetailsMethod string + +const TxlogSchemaJsonTransactionDetailsMethodDELETE TxlogSchemaJsonTransactionDetailsMethod = "DELETE" +const TxlogSchemaJsonTransactionDetailsMethodGET TxlogSchemaJsonTransactionDetailsMethod = "GET" +const TxlogSchemaJsonTransactionDetailsMethodPATCH TxlogSchemaJsonTransactionDetailsMethod = "PATCH" +const TxlogSchemaJsonTransactionDetailsMethodPOST TxlogSchemaJsonTransactionDetailsMethod = "POST" + +// request details of transaction +type TxlogSchemaJsonTransactionDetailsRequest struct { + // check if actor is authenticated + IsAuthenticated bool `json:"is_authenticated"` + + // payload of HTTP transaction + Payload *string `json:"payload,omitempty"` + + // user-agent of http transaction + UserAgent TxlogSchemaJsonTransactionDetailsRequestUserAgent `json:"user-agent,omitempty"` +} + +// user-agent of http transaction +type TxlogSchemaJsonTransactionDetailsRequestUserAgent map[string]interface{} + +// response details of transaction +type TxlogSchemaJsonTransactionDetailsResponse struct { + // response HTTP status of transaction + HttpStatus int `json:"http_status"` + + // response payload/body of transaction + ResponseBody string `json:"response_body"` +} + +var enumValues_TxlogSchemaJsonEntityType = []interface{}{ + "DATABASE", + "COLLECTION", +} +var enumValues_TxlogSchemaJsonStatus = []interface{}{ + "SUCCESS", + "FAILED", +} +var enumValues_TxlogSchemaJsonTransactionDetailsMethod = []interface{}{ + "POST", + "GET", + "DELETE", + "PATCH", +} + +// UnmarshalJSON implements json.Unmarshaler. +func (j *TxlogSchemaJson) UnmarshalJSON(b []byte) error { + var raw map[string]interface{} + if err := json.Unmarshal(b, &raw); err != nil { + return err + } + if v, ok := raw["action"]; !ok || v == nil { + return fmt.Errorf("field action: required") + } + if v, ok := raw["entity_path"]; !ok || v == nil { + return fmt.Errorf("field entity_path: required") + } + if v, ok := raw["entity_type"]; !ok || v == nil { + return fmt.Errorf("field entity_type: required") + } + if v, ok := raw["status"]; !ok || v == nil { + return fmt.Errorf("field status: required") + } + if v, ok := raw["timestamp"]; !ok || v == nil { + return fmt.Errorf("field timestamp: required") + } + if v, ok := raw["transaction_details"]; !ok || v == nil { + return fmt.Errorf("field transaction_details: required") + } + type Plain TxlogSchemaJson + var plain Plain + if err := json.Unmarshal(b, &plain); err != nil { + return err + } + *j = TxlogSchemaJson(plain) + return nil +} diff --git a/internal/txlogs/model/txlog.schema.json b/internal/txlogs/model/txlog.schema.json new file mode 100644 index 0000000..eb72a55 --- /dev/null +++ b/internal/txlogs/model/txlog.schema.json @@ -0,0 +1,126 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Schema for TxLogs", + "type": "object", + "properties": { + "action": { + "description": "action performed on entity, pick from permissions", + "type": "string" + }, + "entity_type": { + "description": "type of entity", + "type": "string", + "enum": [ + "DATABASE", + "COLLECTION" + ] + }, + "entity_path": { + "description": "action performed on entity, pick from permissions", + "type": "object", + "properties": { + "database": { + "description": "database name", + "type": "string" + }, + "collection": { + "description": "collection name, must also have database", + "type": "string" + } + }, + "required": [ + "database" + ] + }, + "status": { + "description": "status of transaction", + "type": "string", + "enum": [ + "SUCCESS", + "FAILED" + ] + }, + "timestamp": { + "description": "timestamp of transaction", + "type": "number" + }, + "actor": { + "description": "actor of transaction", + "type": "string" + }, + "transaction_details": { + "description": "HTTP details of transaction", + "type": "object", + "properties": { + "url_endpoint": { + "description": "transaction endpoint/url", + "type": "string" + }, + "method": { + "description": "HTTP method of transaction", + "type": "string", + "enum": [ + "POST", + "GET", + "DELETE", + "PATCH" + ] + }, + "request": { + "description": "request details of transaction", + "type": "object", + "properties": { + "user-agent": { + "description": "user-agent of http transaction", + "type": "object", + "properties": {} + }, + "is_authenticated": { + "description": "check if actor is authenticated", + "type": "boolean" + }, + "payload": { + "description": "payload of HTTP transaction", + "type": "string" + } + }, + "required": [ + "is_authenticated" + ] + }, + "response": { + "description": "response details of transaction", + "type": "object", + "properties": { + "http_status": { + "description": "response HTTP status of transaction", + "type": "integer" + }, + "response_body": { + "description": "response payload/body of transaction", + "type": "string" + } + }, + "required": [ + "http_status", + "response_body" + ] + } + }, + "required": [ + "url_endpoint", + "method", + "request", + "response" + ] + } + }, + "required": [ + "entity_type", + "action", + "entity_path", + "status", + "timestamp", + "transaction_details" + ] +} \ No newline at end of file diff --git a/internal/txlogs/txlogs.go b/internal/txlogs/txlogs.go new file mode 100644 index 0000000..d1f0bd1 --- /dev/null +++ b/internal/txlogs/txlogs.go @@ -0,0 +1,107 @@ +package txlogs + +import ( + "encoding/json" + "time" + + "github.com/TanmoySG/wunderDB/internal/privileges" + "github.com/TanmoySG/wunderDB/internal/server/response" + txlModel "github.com/TanmoySG/wunderDB/internal/txlogs/model" + "github.com/TanmoySG/wunderDB/internal/users/authentication" + "github.com/gofiber/fiber/v2" +) + +var ( + DatabaseTxnEntity TxnEntityType = "database" + CollectionTxnEntity TxnEntityType = "collection" +) + +type TxnEntityType string + +type TransactionLogs struct { + Logs []*txlModel.TxlogSchemaJson `json:"txn_logs"` +} + +func (tl *TransactionLogs) Marshal() ([]byte, error) { + return json.Marshal(tl) +} + +func CreateTxLog(txnAction, txnActor string, txnRequestStatus string, txnEntities txlModel.TxlogSchemaJsonEntityPath, txnDetails txlModel.TxlogSchemaJsonTransactionDetails) txlModel.TxlogSchemaJson { + return txlModel.TxlogSchemaJson{ + Action: txnAction, + Actor: &txnActor, + EntityPath: txnEntities, + EntityType: getEntityType(txnEntities), + Status: getTxnStatus(txnRequestStatus), + Timestamp: float64(time.Now().Unix()), + TransactionDetails: txnDetails, + } +} + +func GetTxnHttpDetails(c fiber.Ctx) txlModel.TxlogSchemaJsonTransactionDetails { + txnHttpUrl, txnUserAgent, txnRequestIP := c.Path(), c.Get("User-Agent"), c.IP() + + txnRequestHttpMethod := txlModel.TxlogSchemaJsonTransactionDetailsMethod(c.Method()) + txnRequestPayload := string(c.Body()) + + txnResponseHttpStatusCode := c.Response().StatusCode() + txnResponsePayload := string(c.Response().Body()) + + return txlModel.TxlogSchemaJsonTransactionDetails{ + UrlEndpoint: txnHttpUrl, + Method: txnRequestHttpMethod, + Request: txlModel.TxlogSchemaJsonTransactionDetailsRequest{ + IsAuthenticated: true, // make it dynamic based on auth + Payload: &txnRequestPayload, + UserAgent: txlModel.TxlogSchemaJsonTransactionDetailsRequestUserAgent{ + "userAgent": txnUserAgent, // remove hard coded keys + "requestIP": txnRequestIP, + }, + }, + Response: txlModel.TxlogSchemaJsonTransactionDetailsResponse{ + HttpStatus: txnResponseHttpStatusCode, + ResponseBody: txnResponsePayload, + }, + } +} + +func GetTxnActor(authString string) string { + username, _, err := authentication.HandleUserCredentials(authString) + if err != nil { + return "" + } + + return *username +} + +// only write actions are loggable +func IsTxnLoggable(txnAction string) bool { + if txnType := privileges.GetPrivilegeType(txnAction); txnType == privileges.WritePrivilege { + return true + } + return false +} + +func getEntityType(txnEntities txlModel.TxlogSchemaJsonEntityPath) txlModel.TxlogSchemaJsonEntityType { + if txnEntities.Database != "" { + if txnEntities.Collection != nil { + if *txnEntities.Collection != "" { + return txlModel.TxlogSchemaJsonEntityTypeCOLLECTION + } + return txlModel.TxlogSchemaJsonEntityTypeDATABASE // is required ? + } + return txlModel.TxlogSchemaJsonEntityTypeDATABASE + } + return "" +} + +func getTxnStatus(txnRequestStatus string) txlModel.TxlogSchemaJsonStatus { + switch txnRequestStatus { + case response.StatusFailure: + return txlModel.TxlogSchemaJsonStatusFAILED + case response.StatusSuccess: + return txlModel.TxlogSchemaJsonStatusSUCCESS + default: + return "" + } +}