3
0

Implement federation node pairing flow

This commit is contained in:
Tomaž Jerman
2020-10-05 11:47:16 +02:00
committed by Peter Grlica
parent 27463443bd
commit 37e060a576
20 changed files with 1161 additions and 531 deletions

View File

@@ -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

View File

@@ -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)
})
}

View File

@@ -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)
})
}

View File

@@ -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)
})
}

View File

@@ -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)
})
}

61
federation/rest/node.go Normal file
View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
})
}

214
federation/service/node.go Normal file
View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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"`
}
)

71
federation/util/util.go Normal file
View File

@@ -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()
}

View File

@@ -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 {