diff --git a/federation/rest.yaml b/federation/rest.yaml index f819b94fc..04f00d00a 100644 --- a/federation/rest.yaml +++ b/federation/rest.yaml @@ -5,79 +5,106 @@ # Next step: swagger. endpoints: - - title: Node identity - path: "/node/identity" - entrypoint: identity - authentication: [] + - title: Federation node handshake + path: "/nodes/{nodeID}/handshake" + entrypoint: nodeHandshake + parameters: + path: + - name: nodeID + type: uint64 + required: true + title: NodeID apis: - - name: generate node identity + - name: initialize method: POST - title: Generate an origin node identity - path: "/generate" + title: Initialize the handshake step with node B + path: "" parameters: post: - - name: domain + - name: nodeURI type: string required: true - title: Domain of the destination node - - - name: register origin node - method: POST - title: Register a new origin node - path: "/register" - parameters: - post: - - name: identifier + title: Node A node URI + - name: tokenB type: string required: true - title: Origin node identifier - - - title: Federation node pair request - path: "/node/pair/request" - entrypoint: pairRequest - authentication: [] - apis: - - name: request pairing - method: POST - title: Handle destination node pair request - path: "/" - parameters: - post: - - name: identifier - type: string - required: true - title: Origin node identifier - - name: token - type: string - required: true - title: Destination node token - - - title: Federation node pairing - path: "/node/pair" - entrypoint: pair - authentication: [] - apis: - - name: approve pairing - method: POST - title: Approve the destination node pair request - path: "/approve" - parameters: - get: - - name: requestID + title: Node B auth token + - name: nodeIDB type: uint64 required: true - title: Pair requestID + title: Node B nodeID - - name: complete pairing + - title: Federation nodes + path: "/nodes" + entrypoint: node + apis: + - name: create method: POST - title: Complete pairing with the origin - path: "/complete" + title: Create a new federation node + path: "" parameters: post: - - name: token + # @todo remove this + - name: myDomain + type: string + required: false + title: "[TMP] field that determines my domain" + + - name: domain + type: string + required: false + title: Node B domain + - name: name + type: string + required: false + title: Name for this node + - name: adminContact + type: string + required: false + title: Node B admin contact email + - name: nodeURI + type: string + required: false + title: Node A URI + + - name: pair + method: POST + title: Initialize the pairing process between the two nodes + path: "/{nodeID}/pair" + parameters: + path: + - name: nodeID + type: uint64 + required: true + title: NodeID + + - name: handshake confirm + method: POST + title: Confirm the requested handshake + path: "/{nodeID}/handshake-confirm" + parameters: + path: + - name: nodeID + type: uint64 + required: true + title: NodeID + + - name: handshake complete + method: POST + title: Complete the handshake + path: "/{nodeID}/handshake-complete" + parameters: + path: + - name: nodeID + type: uint64 + required: true + title: NodeID + + post: + - name: tokenA type: string required: true - title: Auth token of the origin node + title: Node A token - title: Manage structure description: Manage structure diff --git a/federation/rest/handlers/identity.go b/federation/rest/handlers/identity.go deleted file mode 100644 index 924720a0b..000000000 --- a/federation/rest/handlers/identity.go +++ /dev/null @@ -1,86 +0,0 @@ -package handlers - -// This file is auto-generated. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -// Definitions file that controls how this file is generated: -// - -import ( - "context" - "github.com/go-chi/chi" - "github.com/titpetric/factory/resputil" - "net/http" - - "github.com/cortezaproject/corteza-server/federation/rest/request" - "github.com/cortezaproject/corteza-server/pkg/logger" -) - -type ( - // Internal API interface - IdentityAPI interface { - GenerateNodeIdentity(context.Context, *request.IdentityGenerateNodeIdentity) (interface{}, error) - RegisterOriginNode(context.Context, *request.IdentityRegisterOriginNode) (interface{}, error) - } - - // HTTP API interface - Identity struct { - GenerateNodeIdentity func(http.ResponseWriter, *http.Request) - RegisterOriginNode func(http.ResponseWriter, *http.Request) - } -) - -func NewIdentity(h IdentityAPI) *Identity { - return &Identity{ - GenerateNodeIdentity: func(w http.ResponseWriter, r *http.Request) { - defer r.Body.Close() - params := request.NewIdentityGenerateNodeIdentity() - if err := params.Fill(r); err != nil { - logger.LogParamError("Identity.GenerateNodeIdentity", r, err) - resputil.JSON(w, err) - return - } - - value, err := h.GenerateNodeIdentity(r.Context(), params) - if err != nil { - logger.LogControllerError("Identity.GenerateNodeIdentity", r, err, params.Auditable()) - resputil.JSON(w, err) - return - } - logger.LogControllerCall("Identity.GenerateNodeIdentity", r, params.Auditable()) - if !serveHTTP(value, w, r) { - resputil.JSON(w, value) - } - }, - RegisterOriginNode: func(w http.ResponseWriter, r *http.Request) { - defer r.Body.Close() - params := request.NewIdentityRegisterOriginNode() - if err := params.Fill(r); err != nil { - logger.LogParamError("Identity.RegisterOriginNode", r, err) - resputil.JSON(w, err) - return - } - - value, err := h.RegisterOriginNode(r.Context(), params) - if err != nil { - logger.LogControllerError("Identity.RegisterOriginNode", r, err, params.Auditable()) - resputil.JSON(w, err) - return - } - logger.LogControllerCall("Identity.RegisterOriginNode", r, params.Auditable()) - if !serveHTTP(value, w, r) { - resputil.JSON(w, value) - } - }, - } -} - -func (h Identity) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) { - r.Group(func(r chi.Router) { - r.Use(middlewares...) - r.Post("/node/identity/generate", h.GenerateNodeIdentity) - r.Post("/node/identity/register", h.RegisterOriginNode) - }) -} diff --git a/federation/rest/handlers/node.go b/federation/rest/handlers/node.go new file mode 100644 index 000000000..0fe01bc57 --- /dev/null +++ b/federation/rest/handlers/node.go @@ -0,0 +1,132 @@ +package handlers + +// This file is auto-generated. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// Definitions file that controls how this file is generated: +// + +import ( + "context" + "github.com/go-chi/chi" + "github.com/titpetric/factory/resputil" + "net/http" + + "github.com/cortezaproject/corteza-server/federation/rest/request" + "github.com/cortezaproject/corteza-server/pkg/logger" +) + +type ( + // Internal API interface + NodeAPI interface { + Create(context.Context, *request.NodeCreate) (interface{}, error) + Pair(context.Context, *request.NodePair) (interface{}, error) + HandshakeConfirm(context.Context, *request.NodeHandshakeConfirm) (interface{}, error) + HandshakeComplete(context.Context, *request.NodeHandshakeComplete) (interface{}, error) + } + + // HTTP API interface + Node struct { + Create func(http.ResponseWriter, *http.Request) + Pair func(http.ResponseWriter, *http.Request) + HandshakeConfirm func(http.ResponseWriter, *http.Request) + HandshakeComplete func(http.ResponseWriter, *http.Request) + } +) + +func NewNode(h NodeAPI) *Node { + return &Node{ + Create: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewNodeCreate() + if err := params.Fill(r); err != nil { + logger.LogParamError("Node.Create", r, err) + resputil.JSON(w, err) + return + } + + value, err := h.Create(r.Context(), params) + if err != nil { + logger.LogControllerError("Node.Create", r, err, params.Auditable()) + resputil.JSON(w, err) + return + } + logger.LogControllerCall("Node.Create", r, params.Auditable()) + if !serveHTTP(value, w, r) { + resputil.JSON(w, value) + } + }, + Pair: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewNodePair() + if err := params.Fill(r); err != nil { + logger.LogParamError("Node.Pair", r, err) + resputil.JSON(w, err) + return + } + + value, err := h.Pair(r.Context(), params) + if err != nil { + logger.LogControllerError("Node.Pair", r, err, params.Auditable()) + resputil.JSON(w, err) + return + } + logger.LogControllerCall("Node.Pair", r, params.Auditable()) + if !serveHTTP(value, w, r) { + resputil.JSON(w, value) + } + }, + HandshakeConfirm: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewNodeHandshakeConfirm() + if err := params.Fill(r); err != nil { + logger.LogParamError("Node.HandshakeConfirm", r, err) + resputil.JSON(w, err) + return + } + + value, err := h.HandshakeConfirm(r.Context(), params) + if err != nil { + logger.LogControllerError("Node.HandshakeConfirm", r, err, params.Auditable()) + resputil.JSON(w, err) + return + } + logger.LogControllerCall("Node.HandshakeConfirm", r, params.Auditable()) + if !serveHTTP(value, w, r) { + resputil.JSON(w, value) + } + }, + HandshakeComplete: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewNodeHandshakeComplete() + if err := params.Fill(r); err != nil { + logger.LogParamError("Node.HandshakeComplete", r, err) + resputil.JSON(w, err) + return + } + + value, err := h.HandshakeComplete(r.Context(), params) + if err != nil { + logger.LogControllerError("Node.HandshakeComplete", r, err, params.Auditable()) + resputil.JSON(w, err) + return + } + logger.LogControllerCall("Node.HandshakeComplete", r, params.Auditable()) + if !serveHTTP(value, w, r) { + resputil.JSON(w, value) + } + }, + } +} + +func (h Node) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) { + r.Group(func(r chi.Router) { + r.Use(middlewares...) + r.Post("/nodes", h.Create) + r.Post("/nodes/{nodeID}/pair", h.Pair) + r.Post("/nodes/{nodeID}/handshake-confirm", h.HandshakeConfirm) + r.Post("/nodes/{nodeID}/handshake-complete", h.HandshakeComplete) + }) +} diff --git a/federation/rest/handlers/nodeHandshake.go b/federation/rest/handlers/nodeHandshake.go new file mode 100644 index 000000000..d4757a1ab --- /dev/null +++ b/federation/rest/handlers/nodeHandshake.go @@ -0,0 +1,63 @@ +package handlers + +// This file is auto-generated. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// Definitions file that controls how this file is generated: +// + +import ( + "context" + "github.com/go-chi/chi" + "github.com/titpetric/factory/resputil" + "net/http" + + "github.com/cortezaproject/corteza-server/federation/rest/request" + "github.com/cortezaproject/corteza-server/pkg/logger" +) + +type ( + // Internal API interface + NodeHandshakeAPI interface { + Initialize(context.Context, *request.NodeHandshakeInitialize) (interface{}, error) + } + + // HTTP API interface + NodeHandshake struct { + Initialize func(http.ResponseWriter, *http.Request) + } +) + +func NewNodeHandshake(h NodeHandshakeAPI) *NodeHandshake { + return &NodeHandshake{ + Initialize: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewNodeHandshakeInitialize() + if err := params.Fill(r); err != nil { + logger.LogParamError("NodeHandshake.Initialize", r, err) + resputil.JSON(w, err) + return + } + + value, err := h.Initialize(r.Context(), params) + if err != nil { + logger.LogControllerError("NodeHandshake.Initialize", r, err, params.Auditable()) + resputil.JSON(w, err) + return + } + logger.LogControllerCall("NodeHandshake.Initialize", r, params.Auditable()) + if !serveHTTP(value, w, r) { + resputil.JSON(w, value) + } + }, + } +} + +func (h NodeHandshake) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) { + r.Group(func(r chi.Router) { + r.Use(middlewares...) + r.Post("/nodes/{nodeID}/handshake", h.Initialize) + }) +} diff --git a/federation/rest/handlers/pairRequest.go b/federation/rest/handlers/pairRequest.go deleted file mode 100644 index 7da1cfdf4..000000000 --- a/federation/rest/handlers/pairRequest.go +++ /dev/null @@ -1,63 +0,0 @@ -package handlers - -// This file is auto-generated. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -// Definitions file that controls how this file is generated: -// - -import ( - "context" - "github.com/go-chi/chi" - "github.com/titpetric/factory/resputil" - "net/http" - - "github.com/cortezaproject/corteza-server/federation/rest/request" - "github.com/cortezaproject/corteza-server/pkg/logger" -) - -type ( - // Internal API interface - PairRequestAPI interface { - RequestPairing(context.Context, *request.PairRequestRequestPairing) (interface{}, error) - } - - // HTTP API interface - PairRequest struct { - RequestPairing func(http.ResponseWriter, *http.Request) - } -) - -func NewPairRequest(h PairRequestAPI) *PairRequest { - return &PairRequest{ - RequestPairing: func(w http.ResponseWriter, r *http.Request) { - defer r.Body.Close() - params := request.NewPairRequestRequestPairing() - if err := params.Fill(r); err != nil { - logger.LogParamError("PairRequest.RequestPairing", r, err) - resputil.JSON(w, err) - return - } - - value, err := h.RequestPairing(r.Context(), params) - if err != nil { - logger.LogControllerError("PairRequest.RequestPairing", r, err, params.Auditable()) - resputil.JSON(w, err) - return - } - logger.LogControllerCall("PairRequest.RequestPairing", r, params.Auditable()) - if !serveHTTP(value, w, r) { - resputil.JSON(w, value) - } - }, - } -} - -func (h PairRequest) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) { - r.Group(func(r chi.Router) { - r.Use(middlewares...) - r.Post("/node/pair/request/", h.RequestPairing) - }) -} diff --git a/federation/rest/node.go b/federation/rest/node.go new file mode 100644 index 000000000..9e9feca96 --- /dev/null +++ b/federation/rest/node.go @@ -0,0 +1,61 @@ +package rest + +import ( + "context" + "errors" + + "github.com/cortezaproject/corteza-server/federation/rest/request" + "github.com/cortezaproject/corteza-server/federation/service" + "github.com/cortezaproject/corteza-server/federation/types" +) + +type ( + Node struct { + svcNode service.NodeService + } +) + +var ( + ErrInvalidNodeCreateParams = errors.New("create node: missing or invalid parameters") + ErrorIdentityMissingToken = errors.New("identity: token missing") +) + +func (Node) New() *Node { + return &Node{ + svcNode: service.DefaultNode, + } +} + +func (ctrl Node) Create(ctx context.Context, r *request.NodeCreate) (interface{}, error) { + if r.NodeURI == "" && (r.Domain == "" || r.Name == "") { + return nil, ErrInvalidNodeCreateParams + } + + if r.NodeURI != "" { + return ctrl.svcNode.CreateFromURI(ctx, r.NodeURI, r.MyDomain) + } else if r.Domain != "" && r.Name != "" { + n := &types.Node{} + n.Domain = r.Domain + n.Name = r.Name + n.Status = types.NodeStatusPending + + return ctrl.svcNode.Create(ctx, n, r.MyDomain) + } + + return nil, ErrInvalidNodeCreateParams +} + +func (ctrl Node) Pair(ctx context.Context, r *request.NodePair) (interface{}, error) { + err := ctrl.svcNode.Pair(ctx, r.NodeID) + return nil, err +} + +func (ctrl Node) HandshakeConfirm(ctx context.Context, r *request.NodeHandshakeConfirm) (interface{}, error) { + err := ctrl.svcNode.HandshakeConfirm(ctx, r.NodeID) + return nil, err +} + +func (ctrl Node) HandshakeComplete(ctx context.Context, r *request.NodeHandshakeComplete) (interface{}, error) { + err := ctrl.svcNode.HandshakeComplete(ctx, r.NodeID, r.TokenA) + return nil, err +} diff --git a/federation/rest/node_handshake.go b/federation/rest/node_handshake.go new file mode 100644 index 000000000..11a76001a --- /dev/null +++ b/federation/rest/node_handshake.go @@ -0,0 +1,25 @@ +package rest + +import ( + "context" + + "github.com/cortezaproject/corteza-server/federation/rest/request" + "github.com/cortezaproject/corteza-server/federation/service" +) + +type ( + NodeHandshake struct { + svcNode service.NodeService + } +) + +func (NodeHandshake) New() *NodeHandshake { + return &NodeHandshake{ + svcNode: service.DefaultNode, + } +} + +func (ctrl NodeHandshake) Initialize(ctx context.Context, r *request.NodeHandshakeInitialize) (interface{}, error) { + err := ctrl.svcNode.HandshakeInit(ctx, r.NodeID, r.NodeIDB, r.NodeURI, r.TokenB) + return nil, err +} diff --git a/federation/rest/node_identity.go b/federation/rest/node_identity.go deleted file mode 100644 index c3643ccee..000000000 --- a/federation/rest/node_identity.go +++ /dev/null @@ -1,26 +0,0 @@ -package rest - -import ( - "context" - "fmt" - - "github.com/cortezaproject/corteza-server/federation/rest/request" -) - -type ( - NodeIdentity struct{} -) - -func (NodeIdentity) New() *NodeIdentity { - return &NodeIdentity{} -} - -func (ctrl NodeIdentity) GenerateNodeIdentity(ctx context.Context, r *request.IdentityGenerateNodeIdentity) (interface{}, error) { - fmt.Println("GenerateNOdeIdentity") - return nil, nil -} - -func (ctrl NodeIdentity) RegisterOriginNode(ctx context.Context, r *request.IdentityRegisterOriginNode) (interface{}, error) { - fmt.Println("RegisterORiginNode") - return nil, nil -} diff --git a/federation/rest/node_pair.go b/federation/rest/node_pair.go deleted file mode 100644 index a151d4395..000000000 --- a/federation/rest/node_pair.go +++ /dev/null @@ -1,25 +0,0 @@ -package rest - -import ( - "context" - "fmt" - - "github.com/cortezaproject/corteza-server/federation/rest/request" -) - -type ( - NodePair struct{} -) - -func (NodePair) New() *NodePair { - return &NodePair{} -} - -func (ctrl NodePair) ApprovePairing(ctx context.Context, r *request.PairApprovePairing) (interface{}, error) { - fmt.Println("ApprovePairing") - return nil, nil -} -func (ctrl NodePair) CompletePairing(ctx context.Context, r *request.PairCompletePairing) (interface{}, error) { - fmt.Println("CompletePairing") - return nil, nil -} diff --git a/federation/rest/node_pair_request.go b/federation/rest/node_pair_request.go deleted file mode 100644 index 185cb2d6c..000000000 --- a/federation/rest/node_pair_request.go +++ /dev/null @@ -1,21 +0,0 @@ -package rest - -import ( - "context" - "fmt" - - "github.com/cortezaproject/corteza-server/federation/rest/request" -) - -type ( - NodePairRequest struct{} -) - -func (NodePairRequest) New() *NodePairRequest { - return &NodePairRequest{} -} - -func (ctrl NodePairRequest) RequestPairing(ctx context.Context, r *request.PairRequestRequestPairing) (interface{}, error) { - fmt.Println("RequestPairing") - return nil, nil -} diff --git a/federation/rest/request/identity.go b/federation/rest/request/identity.go deleted file mode 100644 index e4fcc0239..000000000 --- a/federation/rest/request/identity.go +++ /dev/null @@ -1,141 +0,0 @@ -package request - -// This file is auto-generated. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -// Definitions file that controls how this file is generated: -// - -import ( - "encoding/json" - "fmt" - "github.com/cortezaproject/corteza-server/pkg/payload" - "github.com/go-chi/chi" - "io" - "mime/multipart" - "net/http" - "strings" -) - -// dummy vars to prevent -// unused imports complain -var ( - _ = chi.URLParam - _ = multipart.ErrMessageTooLarge - _ = payload.ParseUint64s -) - -type ( - // Internal API interface - IdentityGenerateNodeIdentity struct { - // Domain POST parameter - // - // Domain of the destination node - Domain string - } - - IdentityRegisterOriginNode struct { - // Identifier POST parameter - // - // Origin node identifier - Identifier string - } -) - -// NewIdentityGenerateNodeIdentity request -func NewIdentityGenerateNodeIdentity() *IdentityGenerateNodeIdentity { - return &IdentityGenerateNodeIdentity{} -} - -// Auditable returns all auditable/loggable parameters -func (r IdentityGenerateNodeIdentity) Auditable() map[string]interface{} { - return map[string]interface{}{ - "domain": r.Domain, - } -} - -// Auditable returns all auditable/loggable parameters -func (r IdentityGenerateNodeIdentity) GetDomain() string { - return r.Domain -} - -// Fill processes request and fills internal variables -func (r *IdentityGenerateNodeIdentity) Fill(req *http.Request) (err error) { - if strings.ToLower(req.Header.Get("content-type")) == "application/json" { - err = json.NewDecoder(req.Body).Decode(r) - - switch { - case err == io.EOF: - err = nil - case err != nil: - return fmt.Errorf("error parsing http request body: %w", err) - } - } - - { - if err = req.ParseForm(); err != nil { - return err - } - - // POST params - - if val, ok := req.Form["domain"]; ok && len(val) > 0 { - r.Domain, err = val[0], nil - if err != nil { - return err - } - } - } - - return err -} - -// NewIdentityRegisterOriginNode request -func NewIdentityRegisterOriginNode() *IdentityRegisterOriginNode { - return &IdentityRegisterOriginNode{} -} - -// Auditable returns all auditable/loggable parameters -func (r IdentityRegisterOriginNode) Auditable() map[string]interface{} { - return map[string]interface{}{ - "identifier": r.Identifier, - } -} - -// Auditable returns all auditable/loggable parameters -func (r IdentityRegisterOriginNode) GetIdentifier() string { - return r.Identifier -} - -// Fill processes request and fills internal variables -func (r *IdentityRegisterOriginNode) Fill(req *http.Request) (err error) { - if strings.ToLower(req.Header.Get("content-type")) == "application/json" { - err = json.NewDecoder(req.Body).Decode(r) - - switch { - case err == io.EOF: - err = nil - case err != nil: - return fmt.Errorf("error parsing http request body: %w", err) - } - } - - { - if err = req.ParseForm(); err != nil { - return err - } - - // POST params - - if val, ok := req.Form["identifier"]; ok && len(val) > 0 { - r.Identifier, err = val[0], nil - if err != nil { - return err - } - } - } - - return err -} diff --git a/federation/rest/request/node.go b/federation/rest/request/node.go new file mode 100644 index 000000000..3c1acde36 --- /dev/null +++ b/federation/rest/request/node.go @@ -0,0 +1,340 @@ +package request + +// This file is auto-generated. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// Definitions file that controls how this file is generated: +// + +import ( + "encoding/json" + "fmt" + "github.com/cortezaproject/corteza-server/pkg/payload" + "github.com/go-chi/chi" + "io" + "mime/multipart" + "net/http" + "strings" +) + +// dummy vars to prevent +// unused imports complain +var ( + _ = chi.URLParam + _ = multipart.ErrMessageTooLarge + _ = payload.ParseUint64s +) + +type ( + // Internal API interface + NodeCreate struct { + // MyDomain POST parameter + // + // [TMP] field that determines my domain + MyDomain string + + // Domain POST parameter + // + // Node B domain + Domain string + + // Name POST parameter + // + // Name for this node + Name string + + // AdminContact POST parameter + // + // Node B admin contact email + AdminContact string + + // NodeURI POST parameter + // + // Node A URI + NodeURI string + } + + NodePair struct { + // NodeID PATH parameter + // + // NodeID + NodeID uint64 `json:",string"` + } + + NodeHandshakeConfirm struct { + // NodeID PATH parameter + // + // NodeID + NodeID uint64 `json:",string"` + } + + NodeHandshakeComplete struct { + // NodeID PATH parameter + // + // NodeID + NodeID uint64 `json:",string"` + + // TokenA POST parameter + // + // Node A token + TokenA string + } +) + +// NewNodeCreate request +func NewNodeCreate() *NodeCreate { + return &NodeCreate{} +} + +// Auditable returns all auditable/loggable parameters +func (r NodeCreate) Auditable() map[string]interface{} { + return map[string]interface{}{ + "myDomain": r.MyDomain, + "domain": r.Domain, + "name": r.Name, + "adminContact": r.AdminContact, + "nodeURI": r.NodeURI, + } +} + +// Auditable returns all auditable/loggable parameters +func (r NodeCreate) GetMyDomain() string { + return r.MyDomain +} + +// Auditable returns all auditable/loggable parameters +func (r NodeCreate) GetDomain() string { + return r.Domain +} + +// Auditable returns all auditable/loggable parameters +func (r NodeCreate) GetName() string { + return r.Name +} + +// Auditable returns all auditable/loggable parameters +func (r NodeCreate) GetAdminContact() string { + return r.AdminContact +} + +// Auditable returns all auditable/loggable parameters +func (r NodeCreate) GetNodeURI() string { + return r.NodeURI +} + +// Fill processes request and fills internal variables +func (r *NodeCreate) Fill(req *http.Request) (err error) { + if strings.ToLower(req.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(req.Body).Decode(r) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return fmt.Errorf("error parsing http request body: %w", err) + } + } + + { + if err = req.ParseForm(); err != nil { + return err + } + + // POST params + + if val, ok := req.Form["myDomain"]; ok && len(val) > 0 { + r.MyDomain, err = val[0], nil + if err != nil { + return err + } + } + + if val, ok := req.Form["domain"]; ok && len(val) > 0 { + r.Domain, err = val[0], nil + if err != nil { + return err + } + } + + if val, ok := req.Form["name"]; ok && len(val) > 0 { + r.Name, err = val[0], nil + if err != nil { + return err + } + } + + if val, ok := req.Form["adminContact"]; ok && len(val) > 0 { + r.AdminContact, err = val[0], nil + if err != nil { + return err + } + } + + if val, ok := req.Form["nodeURI"]; ok && len(val) > 0 { + r.NodeURI, err = val[0], nil + if err != nil { + return err + } + } + } + + return err +} + +// NewNodePair request +func NewNodePair() *NodePair { + return &NodePair{} +} + +// Auditable returns all auditable/loggable parameters +func (r NodePair) Auditable() map[string]interface{} { + return map[string]interface{}{ + "nodeID": r.NodeID, + } +} + +// Auditable returns all auditable/loggable parameters +func (r NodePair) GetNodeID() uint64 { + return r.NodeID +} + +// Fill processes request and fills internal variables +func (r *NodePair) Fill(req *http.Request) (err error) { + if strings.ToLower(req.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(req.Body).Decode(r) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return fmt.Errorf("error parsing http request body: %w", err) + } + } + + { + var val string + // path params + + val = chi.URLParam(req, "nodeID") + r.NodeID, err = payload.ParseUint64(val), nil + if err != nil { + return err + } + + } + + return err +} + +// NewNodeHandshakeConfirm request +func NewNodeHandshakeConfirm() *NodeHandshakeConfirm { + return &NodeHandshakeConfirm{} +} + +// Auditable returns all auditable/loggable parameters +func (r NodeHandshakeConfirm) Auditable() map[string]interface{} { + return map[string]interface{}{ + "nodeID": r.NodeID, + } +} + +// Auditable returns all auditable/loggable parameters +func (r NodeHandshakeConfirm) GetNodeID() uint64 { + return r.NodeID +} + +// Fill processes request and fills internal variables +func (r *NodeHandshakeConfirm) Fill(req *http.Request) (err error) { + if strings.ToLower(req.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(req.Body).Decode(r) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return fmt.Errorf("error parsing http request body: %w", err) + } + } + + { + var val string + // path params + + val = chi.URLParam(req, "nodeID") + r.NodeID, err = payload.ParseUint64(val), nil + if err != nil { + return err + } + + } + + return err +} + +// NewNodeHandshakeComplete request +func NewNodeHandshakeComplete() *NodeHandshakeComplete { + return &NodeHandshakeComplete{} +} + +// Auditable returns all auditable/loggable parameters +func (r NodeHandshakeComplete) Auditable() map[string]interface{} { + return map[string]interface{}{ + "nodeID": r.NodeID, + "tokenA": r.TokenA, + } +} + +// Auditable returns all auditable/loggable parameters +func (r NodeHandshakeComplete) GetNodeID() uint64 { + return r.NodeID +} + +// Auditable returns all auditable/loggable parameters +func (r NodeHandshakeComplete) GetTokenA() string { + return r.TokenA +} + +// Fill processes request and fills internal variables +func (r *NodeHandshakeComplete) Fill(req *http.Request) (err error) { + if strings.ToLower(req.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(req.Body).Decode(r) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return fmt.Errorf("error parsing http request body: %w", err) + } + } + + { + if err = req.ParseForm(); err != nil { + return err + } + + // POST params + + if val, ok := req.Form["tokenA"]; ok && len(val) > 0 { + r.TokenA, err = val[0], nil + if err != nil { + return err + } + } + } + + { + var val string + // path params + + val = chi.URLParam(req, "nodeID") + r.NodeID, err = payload.ParseUint64(val), nil + if err != nil { + return err + } + + } + + return err +} diff --git a/federation/rest/request/nodeHandshake.go b/federation/rest/request/nodeHandshake.go new file mode 100644 index 000000000..43e187455 --- /dev/null +++ b/federation/rest/request/nodeHandshake.go @@ -0,0 +1,145 @@ +package request + +// This file is auto-generated. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// Definitions file that controls how this file is generated: +// + +import ( + "encoding/json" + "fmt" + "github.com/cortezaproject/corteza-server/pkg/payload" + "github.com/go-chi/chi" + "io" + "mime/multipart" + "net/http" + "strings" +) + +// dummy vars to prevent +// unused imports complain +var ( + _ = chi.URLParam + _ = multipart.ErrMessageTooLarge + _ = payload.ParseUint64s +) + +type ( + // Internal API interface + NodeHandshakeInitialize struct { + // NodeID PATH parameter + // + // NodeID + NodeID uint64 `json:",string"` + + // NodeURI POST parameter + // + // Node A node URI + NodeURI string + + // TokenB POST parameter + // + // Node B auth token + TokenB string + + // NodeIDB POST parameter + // + // Node B nodeID + NodeIDB uint64 `json:",string"` + } +) + +// NewNodeHandshakeInitialize request +func NewNodeHandshakeInitialize() *NodeHandshakeInitialize { + return &NodeHandshakeInitialize{} +} + +// Auditable returns all auditable/loggable parameters +func (r NodeHandshakeInitialize) Auditable() map[string]interface{} { + return map[string]interface{}{ + "nodeID": r.NodeID, + "nodeURI": r.NodeURI, + "tokenB": r.TokenB, + "nodeIDB": r.NodeIDB, + } +} + +// Auditable returns all auditable/loggable parameters +func (r NodeHandshakeInitialize) GetNodeID() uint64 { + return r.NodeID +} + +// Auditable returns all auditable/loggable parameters +func (r NodeHandshakeInitialize) GetNodeURI() string { + return r.NodeURI +} + +// Auditable returns all auditable/loggable parameters +func (r NodeHandshakeInitialize) GetTokenB() string { + return r.TokenB +} + +// Auditable returns all auditable/loggable parameters +func (r NodeHandshakeInitialize) GetNodeIDB() uint64 { + return r.NodeIDB +} + +// Fill processes request and fills internal variables +func (r *NodeHandshakeInitialize) Fill(req *http.Request) (err error) { + if strings.ToLower(req.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(req.Body).Decode(r) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return fmt.Errorf("error parsing http request body: %w", err) + } + } + + { + if err = req.ParseForm(); err != nil { + return err + } + + // POST params + + if val, ok := req.Form["nodeURI"]; ok && len(val) > 0 { + r.NodeURI, err = val[0], nil + if err != nil { + return err + } + } + + if val, ok := req.Form["tokenB"]; ok && len(val) > 0 { + r.TokenB, err = val[0], nil + if err != nil { + return err + } + } + + if val, ok := req.Form["nodeIDB"]; ok && len(val) > 0 { + r.NodeIDB, err = payload.ParseUint64(val[0]), nil + if err != nil { + return err + } + } + } + + { + var val string + // path params + + val = chi.URLParam(req, "nodeID") + r.NodeID, err = payload.ParseUint64(val), nil + if err != nil { + return err + } + + } + + return err +} diff --git a/federation/rest/request/pairRequest.go b/federation/rest/request/pairRequest.go deleted file mode 100644 index 32fca9484..000000000 --- a/federation/rest/request/pairRequest.go +++ /dev/null @@ -1,104 +0,0 @@ -package request - -// This file is auto-generated. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -// Definitions file that controls how this file is generated: -// - -import ( - "encoding/json" - "fmt" - "github.com/cortezaproject/corteza-server/pkg/payload" - "github.com/go-chi/chi" - "io" - "mime/multipart" - "net/http" - "strings" -) - -// dummy vars to prevent -// unused imports complain -var ( - _ = chi.URLParam - _ = multipart.ErrMessageTooLarge - _ = payload.ParseUint64s -) - -type ( - // Internal API interface - PairRequestRequestPairing struct { - // Identifier POST parameter - // - // Origin node identifier - Identifier string - - // Token POST parameter - // - // Destination node token - Token string - } -) - -// NewPairRequestRequestPairing request -func NewPairRequestRequestPairing() *PairRequestRequestPairing { - return &PairRequestRequestPairing{} -} - -// Auditable returns all auditable/loggable parameters -func (r PairRequestRequestPairing) Auditable() map[string]interface{} { - return map[string]interface{}{ - "identifier": r.Identifier, - "token": r.Token, - } -} - -// Auditable returns all auditable/loggable parameters -func (r PairRequestRequestPairing) GetIdentifier() string { - return r.Identifier -} - -// Auditable returns all auditable/loggable parameters -func (r PairRequestRequestPairing) GetToken() string { - return r.Token -} - -// Fill processes request and fills internal variables -func (r *PairRequestRequestPairing) Fill(req *http.Request) (err error) { - if strings.ToLower(req.Header.Get("content-type")) == "application/json" { - err = json.NewDecoder(req.Body).Decode(r) - - switch { - case err == io.EOF: - err = nil - case err != nil: - return fmt.Errorf("error parsing http request body: %w", err) - } - } - - { - if err = req.ParseForm(); err != nil { - return err - } - - // POST params - - if val, ok := req.Form["identifier"]; ok && len(val) > 0 { - r.Identifier, err = val[0], nil - if err != nil { - return err - } - } - - if val, ok := req.Form["token"]; ok && len(val) > 0 { - r.Token, err = val[0], nil - if err != nil { - return err - } - } - } - - return err -} diff --git a/federation/rest/router.go b/federation/rest/router.go index 833deba14..53960c6e5 100644 --- a/federation/rest/router.go +++ b/federation/rest/router.go @@ -9,7 +9,7 @@ import ( func MountRoutes(r chi.Router) { r.Group(func(r chi.Router) { - handlers.NewPairRequest(NodePairRequest{}.New()).MountRoutes(r) + handlers.NewNodeHandshake(NodeHandshake{}.New()).MountRoutes(r) // temporary because of acl handlers.NewManageStructure((ManageStructure{}.New())).MountRoutes(r) @@ -20,7 +20,6 @@ func MountRoutes(r chi.Router) { r.Use(auth.MiddlewareValidOnly) r.Use(middlewareAllowedAccess) - handlers.NewIdentity(NodeIdentity{}.New()).MountRoutes(r) - handlers.NewPair(NodePair{}.New()).MountRoutes(r) + handlers.NewNode(Node{}.New()).MountRoutes(r) }) } diff --git a/federation/service/node.go b/federation/service/node.go new file mode 100644 index 000000000..519c7ff81 --- /dev/null +++ b/federation/service/node.go @@ -0,0 +1,214 @@ +package service + +import ( + "context" + "errors" + "net/url" + "strconv" + + "github.com/cortezaproject/corteza-server/federation/types" + "github.com/cortezaproject/corteza-server/federation/util" + "github.com/cortezaproject/corteza-server/pkg/actionlog" + "github.com/cortezaproject/corteza-server/pkg/auth" + "github.com/cortezaproject/corteza-server/pkg/id" + "github.com/cortezaproject/corteza-server/pkg/rand" + "github.com/cortezaproject/corteza-server/store" + "github.com/cortezaproject/corteza-server/system/service" + stypes "github.com/cortezaproject/corteza-server/system/types" + "github.com/davecgh/go-spew/spew" +) + +var ( + // This is temporary for local testing + tmpNodeStore = make(types.NodeSet, 0) + + ErrorNodeNotFound = errors.New("pair: node not found") + ErrorInvalidNodeURI = errors.New("pair: invalid node uri provided") +) + +const ( + TokenLength = 15 +) + +type ( + node struct { + store store.Storer + sysUser service.UserService + + actionlog actionlog.Recorder + + tokenEncoder auth.TokenEncoder + } + + NodeService interface { + Create(ctx context.Context, n *types.Node, sharedDomain string) (*types.Node, error) + CreateFromURI(ctx context.Context, uri string, sharedDomain string) (*types.Node, error) + + Pair(ctx context.Context, nodeID uint64) error + HandshakeInit(ctx context.Context, sourceNodeID, nodeIDB uint64, nodeURI, token string) error + HandshakeConfirm(ctx context.Context, nodeID uint64) error + HandshakeComplete(ctx context.Context, nodeID uint64, token string) error + } +) + +func Node(s store.Storer, u service.UserService, al actionlog.Recorder, th auth.TokenHandler) NodeService { + return (&node{ + store: s, + sysUser: u, + actionlog: al, + tokenEncoder: th, + }) +} + +// @todo move myDomain to configuration +func (svc node) Create(ctx context.Context, n *types.Node, myDomain string) (*types.Node, error) { + n.ID = id.Next() + n.Status = types.NodeStatusPending + + ott := string(rand.Bytes(TokenLength)) + i := util.EncodeURI(ott, myDomain, n.ID) + if n.NodeURI == "" { + n.NodeURI = i + } + + // @todo store an initial Node entry + tmpNodeStore = append(tmpNodeStore, n) + + return n, nil +} + +func (svc node) CreateFromURI(ctx context.Context, uri string, sharedDomain string) (*types.Node, error) { + n := &types.Node{ + ID: id.Next(), + Status: types.NodeStatusPending, + } + + uri, err := url.QueryUnescape(uri) + if err != nil { + return nil, err + } + pr, err := util.DecodeURI(uri) + if err != nil { + return nil, err + } + + n.Domain = pr.Domain + n.Name = pr.Params.Name + n.Status = types.NodeStatusPending + n.NodeURI = uri + + // @todo store an initial Node entry + tmpNodeStore = append(tmpNodeStore, n) + + return n, nil +} + +func (svc node) Pair(ctx context.Context, nodeID uint64) error { + // @todo store + n := tmpNodeStore.FindByID(nodeID) + if n == nil { + return ErrorNodeNotFound + } + + // Handle fenedrated user + u, err := svc.fetchFederatedUser(ctx, n) + if err != nil { + return err + } + + // Generate JWT token for the federated user + t := svc.tokenEncoder.Encode(u) + // @todo remove + spew.Dump(u, t) + + // Ping node A to request the handshake + // @todo... + + return nil +} + +func (svc node) HandshakeInit(ctx context.Context, nodeIDA, nodeIDB uint64, nodeURI, token string) error { + // @todo store + n := tmpNodeStore.FindByID(nodeIDA) + if n == nil { + return ErrorNodeNotFound + } + + if n.NodeURI != nodeURI { + return ErrorInvalidNodeURI + } + + // @todo store... + n.Token = token + n.SharedID = nodeIDA + n.Status = types.NodeStatusPairRequest + spew.Dump(n) + + // Notify the node administrator about the request + // @todo + + return nil +} + +func (svc node) HandshakeConfirm(ctx context.Context, nodeID uint64) error { + // @todo store + n := tmpNodeStore.FindByID(nodeID) + if n == nil { + return ErrorNodeNotFound + } + + // Handle fenedrated user + u, err := svc.fetchFederatedUser(ctx, n) + if err != nil { + return err + } + + // Generate JWT token for the federated user + t := svc.tokenEncoder.Encode(u) + // @todo remove + spew.Dump(u, t) + + // Ping node B to complete the handshake + // @todo... + + // @todo store + n.Status = types.NodeStatusPairComplete + + return nil +} + +func (svc node) HandshakeComplete(ctx context.Context, nodeID uint64, token string) error { + // @todo store + n := tmpNodeStore.FindByID(nodeID) + if n == nil { + return ErrorNodeNotFound + } + + // Final update -- update the token and node status + // @todo store + n.Token = token + n.Status = types.NodeStatusPairComplete + + return nil +} + +func (svc node) fetchFederatedUser(ctx context.Context, n *types.Node) (*stypes.User, error) { + // Handle fenedrated user + uHandle := "federation_" + strconv.FormatUint(n.ID, 10) + u, _ := svc.sysUser.With(ctx).FindByHandle(uHandle) + + if u == nil { + var err error + + // Create a system user to service this node + u, err = svc.sysUser.With(ctx).Create(&stypes.User{ + Email: strconv.FormatUint(n.ID, 10) + "@federation.corteza", + Handle: uHandle, + Kind: stypes.FederationUser, + }) + if err != nil { + return nil, err + } + } + return u, nil +} diff --git a/federation/service/service.go b/federation/service/service.go index 46d50a71d..2e35bae7e 100644 --- a/federation/service/service.go +++ b/federation/service/service.go @@ -5,6 +5,7 @@ import ( "time" "github.com/cortezaproject/corteza-server/pkg/actionlog" + "github.com/cortezaproject/corteza-server/pkg/auth" "github.com/cortezaproject/corteza-server/pkg/healthcheck" "github.com/cortezaproject/corteza-server/pkg/id" "github.com/cortezaproject/corteza-server/pkg/objstore" @@ -12,6 +13,7 @@ import ( "github.com/cortezaproject/corteza-server/pkg/objstore/plain" "github.com/cortezaproject/corteza-server/pkg/options" "github.com/cortezaproject/corteza-server/store" + "github.com/cortezaproject/corteza-server/system/service" "github.com/cortezaproject/corteza-server/system/types" "go.uber.org/zap" ) @@ -38,6 +40,8 @@ var ( DefaultActionlog actionlog.Recorder + DefaultNode NodeService + // wrapper around time.Now() that will aid service testing now = func() *time.Time { c := time.Now() @@ -112,5 +116,7 @@ func Initialize(ctx context.Context, log *zap.Logger, s store.Storer, c Config) hcd.Add(objstore.Healthcheck(DefaultObjectStore), "Store/Federation") + DefaultNode = Node(DefaultStore, service.DefaultUser, DefaultActionlog, auth.DefaultJwtHandler) + return } diff --git a/federation/types/node.go b/federation/types/node.go index b25f1e23e..a698800ab 100644 --- a/federation/types/node.go +++ b/federation/types/node.go @@ -1,8 +1,19 @@ package types +var ( + NodeStatusPending = "pending" + NodeStatusPairRequest = "pair_requested" + NodeStatusPairComplete = "paired" +) + type ( Node struct { - ID uint64 `json:"recordID,string"` - Name string `json:"name,string"` + ID uint64 `json:"nodeID,string"` + SharedID uint64 `json:"sharedNodeID,string"` + Name string `json:"name,string"` + Domain string `json:"domain,string"` + Status string `json:"status,string"` + Token string `json:"-"` + NodeURI string `json:"nodeURI,string"` } ) diff --git a/federation/util/util.go b/federation/util/util.go new file mode 100644 index 000000000..ed3a7d983 --- /dev/null +++ b/federation/util/util.go @@ -0,0 +1,71 @@ +package util + +import ( + "errors" + "net/url" + "strconv" +) + +var ( + ErrorURIMissingToken = errors.New("uri: token missing") +) + +const () + +type ( + NodeURIParams struct { + Name string + } + + DecodedURI struct { + Domain string + Token string + NodeID uint64 + Params NodeURIParams + } +) + +func DecodeURI(i string) (du *DecodedURI, err error) { + du = &DecodedURI{ + Params: NodeURIParams{}, + } + + u, err := url.Parse(i) + if err != nil { + return + } + + du.Domain = u.Host + nodeS := u.User.Username() + if nodeS == "" { + return nil, ErrorURIMissingToken + } + nodeID, err := strconv.ParseUint(nodeS, 10, 64) + if err != nil { + return nil, err + } + du.NodeID = nodeID + + token, has := u.User.Password() + if !has { + return nil, ErrorURIMissingToken + } + du.Token = token + + params := u.Query() + if name, has := params["name"]; has && len(name) > 0 { + du.Params.Name = name[0] + } + + return +} + +func EncodeURI(ott, domain string, nodeID uint64) string { + u := url.URL{ + Scheme: "corteza", + User: url.UserPassword(strconv.FormatUint(nodeID, 10), ott), + Host: domain, + } + + return u.String() +} diff --git a/system/types/user.go b/system/types/user.go index 04bce5d8a..6751dd611 100644 --- a/system/types/user.go +++ b/system/types/user.go @@ -4,9 +4,10 @@ import ( "database/sql/driver" "encoding/json" "fmt" - "github.com/cortezaproject/corteza-server/pkg/filter" "time" + "github.com/cortezaproject/corteza-server/pkg/filter" + "github.com/pkg/errors" "github.com/cortezaproject/corteza-server/pkg/rbac" @@ -83,8 +84,9 @@ type ( ) const ( - NormalUser UserKind = "" - BotUser UserKind = "bot" + NormalUser UserKind = "" + BotUser UserKind = "bot" + FederationUser UserKind = "fdr" ) func (u User) String() string {