Bootstrap copied BFF and local accounts with public Sendico contracts (#34)

This commit is contained in:
Stephan D committed 2026-09-12 13:37:48 +02:00
1 parent 67dc5b5b1e
commit 7062672a87
556 files changed
+42291 -777

No files matched your search

@@ -0,0 +1,42 @@
package architecture
import (
"go/parser"
"go/token"
"io/fs"
"path/filepath"
"strconv"
"strings"
"testing"
)
// Provider access is HTTP only. Local Mongo repositories belong to SXCHNGE.
func TestNoSendicoRuntimeOrPrivateTransportImports(t *testing.T) {
err := filepath.WalkDir("../..", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() || filepath.Ext(path) != ".go" {
return nil
}
f, err := parser.ParseFile(token.NewFileSet(), path, nil, parser.ImportsOnly)
if err != nil {
return err
}
for _, im := range f.Imports {
name, err := strconv.Unquote(im.Path.Value)
if err != nil {
return err
}
for _, blocked := range []string{"github.com/tech/sendico/", "google.golang.org/grpc", "github.com/nats-io/", "/pkg/discovery", "/pkg/proto/"} {
if strings.Contains(name, blocked) {
t.Errorf("%s imports private provider transport or shared runtime: %s", path, name)
}
}
}
return nil
})
if err != nil {
t.Fatal(err)
}
}
@@ -0,0 +1,60 @@
package architecture
import (
"gopkg.in/yaml.v3"
"io/fs"
"os"
"path/filepath"
"strings"
"testing"
)
func TestPublicContractReferencesAreSelfContained(t *testing.T) {
root := filepath.Join("..", "..", "contracts", "sendico")
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() || filepath.Ext(path) != ".yaml" {
return nil
}
data, err := os.ReadFile(path)
if err != nil {
return err
}
var doc yaml.Node
if err := yaml.Unmarshal(data, &doc); err != nil {
return err
}
var visit func(*yaml.Node)
visit = func(n *yaml.Node) {
if n.Kind == yaml.MappingNode {
for i := 0; i+1 < len(n.Content); i += 2 {
if n.Content[i].Value == "$ref" {
ref := strings.SplitN(n.Content[i+1].Value, "#", 2)[0]
if ref == "" {
continue
}
target := filepath.Clean(filepath.Join(filepath.Dir(path), ref))
relative, err := filepath.Rel(root, target)
if err != nil || strings.HasPrefix(relative, "..") || strings.Contains(ref, "://") {
t.Errorf("%s: nonlocal reference %s", path, ref)
continue
}
if _, err := os.Stat(target); err != nil {
t.Errorf("%s: missing reference %s", path, ref)
}
}
}
}
for _, child := range n.Content {
visit(child)
}
}
visit(&doc)
return nil
})
if err != nil {
t.Fatal(err)
}
}
@@ -0,0 +1,69 @@
package architecture
import (
"go/ast"
"go/parser"
"go/token"
"io/fs"
"path/filepath"
"strconv"
"testing"
)
// Keep the backend error-model guideline executable, including aliased imports.
func TestErrorConstructorsUseProjectModel(t *testing.T) {
root := filepath.Join("..", "..")
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() || filepath.Ext(path) != ".go" {
return nil
}
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, path, nil, 0)
if err != nil {
return err
}
imports := map[string]string{}
for _, imp := range file.Imports {
pkg, err := strconv.Unquote(imp.Path.Value)
if err != nil {
return err
}
if pkg != "errors" && pkg != "fmt" {
continue
}
alias := pkg
if imp.Name != nil {
alias = imp.Name.Name
}
if alias == "." {
t.Errorf("%s: dot import obscures error constructors", path)
}
imports[alias] = pkg
}
ast.Inspect(file, func(n ast.Node) bool {
call, ok := n.(*ast.CallExpr)
if !ok {
return true
}
sel, ok := call.Fun.(*ast.SelectorExpr)
if !ok {
return true
}
ident, ok := sel.X.(*ast.Ident)
if !ok {
return true
}
if (imports[ident.Name] == "errors" && sel.Sel.Name == "New") || (imports[ident.Name] == "fmt" && sel.Sel.Name == "Errorf") {
t.Errorf("%s: use backend/pkg/merrors", fset.Position(call.Pos()))
}
return true
})
return nil
})
if err != nil {
t.Fatal(err)
}
}
-118
View File
@@ -1,118 +0,0 @@
// Adapted from Sendico edge/shared/auth/browser; see backend/PROVENANCE.md.
package browser
import (
"errors"
"net/http"
"net/url"
"strings"
"time"
)
const (
SessionCookie = "__Host-sxchnge_session"
CSRFHeader = "X-CSRF-Protection"
CSRFValue = "1"
)
var (
ErrOrigin = errors.New("browser origin is not allowed")
ErrCSRF = errors.New("browser CSRF protection is required")
ErrSession = errors.New("browser session is missing or invalid")
)
type Policy struct{ origins map[string]struct{} }
// New accepts explicit HTTPS origins. Browser session transport is chosen by the
// server route; a client-controlled transport header cannot turn protection off.
func New(allowedOrigins []string) (*Policy, error) {
p := &Policy{origins: make(map[string]struct{}, len(allowedOrigins))}
for _, value := range allowedOrigins {
origin, ok := canonicalOrigin(value)
if !ok {
return nil, ErrOrigin
}
p.origins[origin] = struct{}{}
}
if len(p.origins) == 0 {
return nil, ErrOrigin
}
return p, nil
}
func canonicalOrigin(value string) (string, bool) {
u, err := url.Parse(value)
if err != nil || u.Scheme != "https" || u.Hostname() == "" || u.User != nil || u.Path != "" || u.RawQuery != "" || u.ForceQuery || u.Fragment != "" || u.Opaque != "" {
return "", false
}
return "https://" + strings.ToLower(u.Host), true
}
// Validate must guard every browser-session mutation, including login/logout.
// It assumes CORS never grants credentialed access to untrusted origins.
func (p *Policy) Validate(r *http.Request) error {
if r == nil {
return ErrCSRF
}
switch r.Method {
case http.MethodGet, http.MethodHead, http.MethodOptions:
return nil
}
if r.Header.Get(CSRFHeader) != CSRFValue {
return ErrCSRF
}
origin, ok := canonicalOrigin(r.Header.Get("Origin"))
if !ok || p == nil {
return ErrOrigin
}
if _, allowed := p.origins[origin]; !allowed {
return ErrOrigin
}
return nil
}
func Credential(r *http.Request) (string, error) {
if r == nil {
return "", ErrSession
}
var value string
for _, c := range r.Cookies() {
if c.Name != SessionCookie {
continue
}
if value != "" || c.Value == "" {
return "", ErrSession
}
value = c.Value
}
if value == "" {
return "", ErrSession
}
return value, nil
}
func SetSession(w http.ResponseWriter, token string, now, expires time.Time) error {
age := int(expires.Sub(now).Seconds())
if token == "" || age < 1 {
return ErrSession
}
c := cookie(token)
c.Expires = expires.UTC()
c.MaxAge = age
if c.Valid() != nil {
return ErrSession
}
http.SetCookie(w, c)
return nil
}
func ClearSession(w http.ResponseWriter) {
c := cookie("")
c.Expires = time.Unix(1, 0).UTC()
c.MaxAge = -1
http.SetCookie(w, c)
}
func cookie(value string) *http.Cookie {
return &http.Cookie{Name: SessionCookie, Value: value, Path: "/", HttpOnly: true, Secure: true, SameSite: http.SameSiteLaxMode}
}
-79
View File
@@ -1,79 +0,0 @@
package browser
import (
"net/http"
"net/http/httptest"
"testing"
"time"
)
func TestMutationRequiresOriginAndHeader(t *testing.T) {
p, err := New([]string{"https://app.example.com"})
if err != nil {
t.Fatal(err)
}
for _, tc := range []struct {
origin, header, transport string
valid bool
}{
{"https://app.example.com", "1", "", true},
{"https://APP.example.com", "1", "", true},
{"https://evil.example.com", "1", "", false},
{"https://app.example.com.evil.test", "1", "", false},
{"https://app.example.com/path", "1", "", false},
{"https://app.example.com?query", "1", "", false},
{"https://app.example.com#fragment", "1", "", false},
{"https://app.example.com", "", "bearer", false},
{"", "1", "bearer", false},
{"null", "1", "cookie", false},
} {
r := httptest.NewRequest(http.MethodPost, "https://app.example.com/api/session", nil)
r.Header.Set("Origin", tc.origin)
r.Header.Set(CSRFHeader, tc.header)
r.Header.Set("X-Auth-Transport", tc.transport)
if got := p.Validate(r); (got == nil) != tc.valid {
t.Errorf("%+v: %v", tc, got)
}
}
if p.Validate(httptest.NewRequest(http.MethodGet, "https://app.example.com/api/session", nil)) != nil {
t.Fatal("read rejected")
}
for _, origin := range []string{"*", "http://app.example.com", "https://user@app.example.com", "https://app.example.com/"} {
if _, err := New([]string{origin}); err == nil {
t.Errorf("accepted configuration %q", origin)
}
}
if _, err := New(nil); err == nil {
t.Fatal("empty origin policy accepted")
}
}
func TestSessionCookie(t *testing.T) {
now := time.Date(2026, 9, 12, 0, 0, 0, 0, time.UTC)
w := httptest.NewRecorder()
if err := SetSession(w, "opaque-token", now, now.Add(time.Hour)); err != nil {
t.Fatal(err)
}
c := w.Result().Cookies()[0]
if c.Name != SessionCookie || !c.Secure || !c.HttpOnly || c.Path != "/" || c.Domain != "" || c.SameSite != http.SameSiteLaxMode || c.MaxAge != 3600 {
t.Fatalf("bad cookie: %+v", c)
}
r := httptest.NewRequest(http.MethodGet, "https://app.example.com/api/session", nil)
r.AddCookie(c)
if value, err := Credential(r); err != nil || value != "opaque-token" {
t.Fatal("cookie not read")
}
r.AddCookie(c)
if _, err := Credential(r); err == nil {
t.Fatal("ambiguous credential accepted")
}
if err := SetSession(httptest.NewRecorder(), "token", now, now); err == nil {
t.Fatal("expired cookie accepted")
}
w = httptest.NewRecorder()
ClearSession(w)
c = w.Result().Cookies()[0]
if c.MaxAge != -1 || c.Value != "" || c.Path != "/" || !c.Secure {
t.Fatal("cookie not cleared safely")
}
}
-29
View File
@@ -1,29 +0,0 @@
// Adapted from Sendico verification/hash.go; see backend/PROVENANCE.md.
package credential
import (
"crypto/rand"
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
)
// New returns an opaque credential and its storage digest. Store only the digest.
// This is for high-entropy tokens, never passwords or short numeric OTP codes.
func New() (raw, digest string, err error) {
bytes := make([]byte, 32)
if _, err = rand.Read(bytes); err != nil {
return "", "", err
}
raw = base64.RawURLEncoding.EncodeToString(bytes)
return raw, Digest(raw), nil
}
func Digest(raw string) string {
sum := sha256.Sum256([]byte(raw))
return base64.RawURLEncoding.EncodeToString(sum[:])
}
func Matches(raw, digest string) bool {
return raw != "" && subtle.ConstantTimeCompare([]byte(Digest(raw)), []byte(digest)) == 1
}
-24
View File
@@ -1,24 +0,0 @@
package credential
import (
"encoding/base64"
"testing"
)
func TestOpaqueCredential(t *testing.T) {
raw, digest, err := New()
if err != nil {
t.Fatal(err)
}
decoded, err := base64.RawURLEncoding.DecodeString(raw)
if err != nil || len(decoded) != 32 {
t.Fatal("credential must contain 256 random bits")
}
if raw == digest || !Matches(raw, digest) || Matches("other", digest) || Matches("", Digest("")) {
t.Fatal("invalid digest matching behavior")
}
other, _, err := New()
if err != nil || other == raw {
t.Fatal("credentials must be independently generated")
}
}
-43
View File
@@ -1,43 +0,0 @@
// Adapted from Sendico edge/shared/storage/s3key; see backend/PROVENANCE.md.
package evidence
import (
"errors"
"path"
"strings"
"unicode"
)
var ErrInvalidKey = errors.New("invalid evidence key")
// JoinKey confines a server-generated object reference to a relative prefix.
// It is not an ownership check or a local-filesystem path sanitizer. Inputs must
// not be URL-decoded or interpreted as filesystem paths after validation.
func JoinKey(prefix, reference string) (string, error) {
a, err := relative(prefix)
if err != nil {
return "", err
}
b, err := relative(reference)
if err != nil {
return "", err
}
return path.Join(a, b), nil
}
func relative(value string) (string, error) {
value = strings.TrimSpace(value)
if strings.ContainsAny(value, "\\%") || strings.ContainsFunc(value, unicode.IsControl) {
return "", ErrInvalidKey
}
for _, segment := range strings.Split(value, "/") {
if segment == ".." {
return "", ErrInvalidKey
}
}
clean := path.Clean(value)
if value == "" || clean == "." || path.IsAbs(clean) {
return "", ErrInvalidKey
}
return clean, nil
}
-18
View File
@@ -1,18 +0,0 @@
package evidence
import "testing"
func TestKeyConfinement(t *testing.T) {
for _, ref := range []string{"", ".", "..", "../other", "/other", "a/../../other", "a\\..\\other", "%2e%2e/other", "bad\x00key"} {
if _, err := JoinKey("customer/documents", ref); err == nil {
t.Errorf("accepted %q", ref)
}
if _, err := JoinKey(ref, "document"); err == nil {
t.Errorf("accepted prefix %q", ref)
}
}
got, err := JoinKey("customer/documents/", "passport/front")
if err != nil || got != "customer/documents/passport/front" {
t.Fatalf("got %q, %v", got, err)
}
}
-28
View File
@@ -1,28 +0,0 @@
package httpapi
import (
"encoding/json"
"net/http"
)
// New is the operational shell. Product routes will be added only with their
// persistence, authentication and provider adapters, never simulated success.
func New() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
write(w, http.StatusOK, "ok")
})
mux.HandleFunc("GET /readyz", func(w http.ResponseWriter, r *http.Request) {
write(w, http.StatusServiceUnavailable, "not_configured")
})
return mux
}
func write(w http.ResponseWriter, status int, value string) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(struct {
Status string `json:"status"`
}{value})
}
-17
View File
@@ -1,17 +0,0 @@
package httpapi
import (
"net/http/httptest"
"testing"
)
func TestOperationalShellDoesNotClaimReadiness(t *testing.T) {
handler := New()
for path, want := range map[string]int{"/healthz": 200, "/readyz": 503, "/api/v1/session": 404} {
w := httptest.NewRecorder()
handler.ServeHTTP(w, httptest.NewRequest("GET", path, nil))
if w.Code != want {
t.Errorf("%s: %d, want %d", path, w.Code, want)
}
}
}
+22
View File
@@ -0,0 +1,22 @@
// Package profile selects the locally owned components of the copied BFF.
package profile
import (
"git.sendico.io/tech/sxchnge/bff/edge/product/interface/accountservice"
"git.sendico.io/tech/sxchnge/bff/edge/shared/interface/api"
)
const Accounts = "accounts"
func Runtime() api.RuntimeConfig {
return api.RuntimeConfig{Name: "sxchnge.bff", Bootstrap: bootstrap}
}
func bootstrap(a api.API) (api.RuntimeComponents, error) {
permissions := a.Permissions()
accounts, err := accountservice.NewAccountService(a.Logger(), a.DBFactory(), permissions.Enforcer(), permissions.Manager().Role(), &a.Config().Mw.Password)
if err != nil {
return nil, err
}
return api.RuntimeComponents{Accounts: accounts}, nil
}