diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4984123..963d182 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,8 +4,8 @@ jobs: test: strategy: matrix: - go-version: [ 1.13.x, 1.14.x, 1.15.x ] - os: [ ubuntu-latest ] + go-version: [ 1.13.x, 1.14.x, 1.15.x, 1.16.x ] + os: [ ubuntu-20.04 ] runs-on: ${{ matrix.os }} steps: - name: Install Go @@ -38,9 +38,7 @@ jobs: - name: Go test run: | go test -race -coverprofile=coverage.txt -covermode=atomic ./... - - name: Upload coverage report - uses: codecov/codecov-action@v1 + - name: Upload coverage report to coverall + uses: shogo82148/actions-goveralls@v1 with: - file: ./coverage.txt - flags: unittests - name: codecov-umbrella + path-to-profile: coverage.txt diff --git a/.github/workflows/deploy_staging.yml b/.github/workflows/deploy_staging.yml index f8377b7..a4a5278 100644 --- a/.github/workflows/deploy_staging.yml +++ b/.github/workflows/deploy_staging.yml @@ -16,7 +16,8 @@ jobs: apt-get install -y zip awscli - name: Build and zip run: | - CGO_ENABLED=0 GOOS=linux go build -o main cmd/lambda/main.go + COMMIT=$(shell git rev-parse HEAD) + CGO_ENABLED=0 GOOS=linux go build -o main -X 'github.com/bitmaelum/key-resolver-go/internal.GitCommit=${COMMIT}' cmd/lambda/main.go zip -r ./function.zip main - name: deploy zip to lambda run: | diff --git a/README.md b/README.md index 5b3677f..3661e84 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,16 @@ +logo + [![Go Report Card](https://goreportcard.com/badge/github.com/bitmaelum/key-resolver-go)](https://goreportcard.com/report/github.com/bitmaelum/key-resolver-go) -![BitMaelum CI](https://github.com/bitmaelum/key-resolver-go/workflows/BitMaelum%20CI/badge.svg?branch=develop) -[![codecov](https://codecov.io/gh/bitmaelum/key-resolver-go/branch/develop/graph/badge.svg)](https://codecov.io/gh/bitmaelum/key-resolver-go) +[![BitMaelum Key Resolver](https://github.com/bitmaelum/key-resolver-go/actions/workflows/ci.yml/badge.svg)](https://github.com/bitmaelum/key-resolver-go/actions/workflows/ci.yml) +[![Coverage Status](https://coveralls.io/repos/github/bitmaelum/key-resolver-go/badge.svg?branch=master)](https://coveralls.io/github/bitmaelum/key-resolver-go?branch=master) ![License](https://img.shields.io/github/license/bitmaelum/key-resolver-go) ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/bitmaelum/key-resolver-go) -[![Gitter](https://badges.gitter.im/bitmaelum/community.svg)](https://gitter.im/bitmaelum/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=bitmaelum_bitmaelum-suite&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=bitmaelum_bitmaelum-suite) - - ____ _ _ __ __ _ - | _ \(_) | | \/ | | | - | |_) |_| |_| \ / | __ _ ___| |_ _ _ __ ___ - | _ <| | __| |\/| |/ _` |/ _ \ | | | | '_ ` _ \ - | |_) | | |_| | | | (_| | __/ | |_| | | | | | | - |____/|_|\__|_| |_|\__,_|\___|_|\__,_|_| |_| |_| - P r i v a c y i s y o u r s a g a i n -# Key resolver +
-[![codecov](https://codecov.io/gh/bitmaelum/key-resolver-go/branch/develop/graph/badge.svg?token=IHXRZZO8KQ)](undefined) +# Key resolver This repository holds the (centralized) account and routing resolver for BitMaelum. diff --git a/cmd/bm-keyresolver/main.go b/cmd/bm-keyresolver/main.go index b9c285e..4dc830f 100644 --- a/cmd/bm-keyresolver/main.go +++ b/cmd/bm-keyresolver/main.go @@ -117,6 +117,9 @@ func main() { router.HandleFunc("/address/{hash}", requestWrapper(handler.DeleteAddressHash)).Methods("DELETE") router.HandleFunc("/address/{hash}", requestWrapper(handler.PostAddressHash)).Methods("POST") + router.HandleFunc("/address/{hash}/status/{fingerprint}", requestWrapper(handler.GetKeyStatus)).Methods("GET") + router.HandleFunc("/address/{hash}/status/{fingerprint}", requestWrapper(handler.SetKeyStatus)).Methods("POST") + router.HandleFunc("/routing/{hash}", requestWrapper(handler.GetRoutingHash)).Methods("GET") router.HandleFunc("/routing/{hash}", requestWrapper(handler.DeleteRoutingHash)).Methods("DELETE") router.HandleFunc("/routing/{hash}", requestWrapper(handler.PostRoutingHash)).Methods("POST") diff --git a/cmd/lambda/main.go b/cmd/lambda/main.go index 7a3b756..679ae9a 100644 --- a/cmd/lambda/main.go +++ b/cmd/lambda/main.go @@ -34,6 +34,26 @@ import ( "github.com/bitmaelum/key-resolver-go/internal/http" ) +type HandlerFunc func(hash.Hash, http.Request) *http.Response + +var handlerMapping = map[string]HandlerFunc{ + "GET /address/{hash}": handler.GetAddressHash, + "POST /address/{hash}/delete": handler.SoftDeleteAddressHash, + "POST /address/{hash}/undelete": handler.SoftUndeleteAddressHash, + "GET /address/{hash}/status/{fingerprint}": handler.GetKeyStatus, + "POST /address/{hash}/status/{fingerprint}": handler.SetKeyStatus, + "DELETE /address/{hash}": handler.DeleteAddressHash, + "POST /address/{hash}": handler.PostAddressHash, + "GET /routing/{hash}": handler.GetRoutingHash, + "DELETE /routing/{hash}": handler.DeleteRoutingHash, + "POST /routing/{hash}": handler.PostRoutingHash, + "GET /organisation/{hash}": handler.GetOrganisationHash, + "POST /organisation/{hash}/delete": handler.SoftDeleteOrganisationHash, + "POST /organisation/{hash}/undelete": handler.SoftUndeleteOrganisationHash, + "DELETE /organisation/{hash}": handler.DeleteOrganisationHash, + "POST /organisation/{hash}": handler.PostOrganisationHash, +} + // HandleRequest checks the incoming route and calls the correct handler for it func HandleRequest(req events.APIGatewayV2HTTPRequest) (*events.APIGatewayV2HTTPResponse, error) { if req.RouteKey == "GET /" { @@ -53,30 +73,10 @@ func HandleRequest(req events.APIGatewayV2HTTPRequest) (*events.APIGatewayV2HTTP var httpResp *http.Response httpReq := apigateway.ReqToHTTP(&req) - switch req.RouteKey { - // Address endpoints - case "GET /address/{hash}": - httpResp = handler.GetAddressHash(*h, *httpReq) - case "DELETE /address/{hash}": - httpResp = handler.DeleteAddressHash(*h, *httpReq) - case "POST /address/{hash}": - httpResp = handler.PostAddressHash(*h, *httpReq) - - // Routing endpoints - case "GET /routing/{hash}": - httpResp = handler.GetRoutingHash(*h, *httpReq) - case "DELETE /routing/{hash}": - httpResp = handler.DeleteRoutingHash(*h, *httpReq) - case "POST /routing/{hash}": - httpResp = handler.PostRoutingHash(*h, *httpReq) - - // Organisation endpoints - case "GET /organisation/{hash}": - httpResp = handler.GetOrganisationHash(*h, *httpReq) - case "DELETE /organisation/{hash}": - httpResp = handler.DeleteOrganisationHash(*h, *httpReq) - case "POST /organisation/{hash}": - httpResp = handler.PostOrganisationHash(*h, *httpReq) + // Check mapping and call correct handler func + f, ok := handlerMapping[req.RouteKey] + if ok { + httpResp = f(*h, *httpReq) } if httpResp == nil { diff --git a/cmd/lambda/main_test.go b/cmd/lambda/main_test.go index d48ae85..8507cb6 100644 --- a/cmd/lambda/main_test.go +++ b/cmd/lambda/main_test.go @@ -69,3 +69,16 @@ func TestHandleRequest404(t *testing.T) { assert.Equal(t, "application/json", res.Headers["Content-Type"]) assert.Equal(t, "{\n \"error\": \"Forbidden\"\n}", res.Body) } + +func TestHandleConfig(t *testing.T) { + req := &events.APIGatewayV2HTTPRequest{ + RouteKey: "GET /config.json", + } + + res, err := HandleRequest(*req) + assert.NoError(t, err) + + assert.Equal(t, 200, res.StatusCode) + assert.Equal(t, "application/json", res.Headers["Content-Type"]) + assert.JSONEq(t, res.Body, "{\"proof_of_work\":{\"address\": 27,\"organisation\":29}}") +} diff --git a/go.mod b/go.mod index 1391765..88fd405 100644 --- a/go.mod +++ b/go.mod @@ -6,9 +6,9 @@ require ( github.com/aws/aws-lambda-go v1.19.1 github.com/aws/aws-sdk-go v1.34.14 github.com/bitmaelum/bitmaelum-suite v0.0.0-20201115094342-919e00359ffc - github.com/boltdb/bolt v1.3.1 github.com/gorilla/mux v1.7.4 github.com/gusaul/go-dynamock v0.0.0-20200325102056-aaeeb0c0e9c1 github.com/mattn/go-sqlite3 v1.14.1 github.com/stretchr/testify v1.6.1 + go.etcd.io/bbolt v1.3.5 ) diff --git a/go.sum b/go.sum index 2ed6c30..feb5571 100644 --- a/go.sum +++ b/go.sum @@ -15,8 +15,6 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/bitmaelum/bitmaelum-suite v0.0.0-20201115094342-919e00359ffc h1:12qg9x0s9a38ru7UvggYKxTFnVt3zlDc5xwr8sJY4OY= github.com/bitmaelum/bitmaelum-suite v0.0.0-20201115094342-919e00359ffc/go.mod h1:t5Rc5fsWnZsjIh3S2PW4xgl07+rPr/CA3QO1apvDHSc= -github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4= -github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= github.com/c2h5oh/datasize v0.0.0-20200112174442-28bbd4740fee/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -183,6 +181,7 @@ github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/zalando/go-keyring v0.1.0/go.mod h1:RaxNwUITJaHVdQ0VC7pELPZ3tOWn13nr0gZMZEhpVU0= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.opentelemetry.io/otel v0.11.0/go.mod h1:G8UCk+KooF2HLkgo8RHX9epABH/aRGYET7gQOqBVdB0= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= @@ -259,6 +258,7 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= diff --git a/internal/address/bolt.go b/internal/address/bolt.go index fa8a941..cbb7715 100644 --- a/internal/address/bolt.go +++ b/internal/address/bolt.go @@ -23,21 +23,21 @@ import ( "encoding/json" "time" + "github.com/bitmaelum/bitmaelum-suite/pkg/bmcrypto" "github.com/bitmaelum/key-resolver-go/internal" - "github.com/boltdb/bolt" + bolt "go.etcd.io/bbolt" ) type boltResolver struct { client *bolt.DB - bucketName string + bucketName []byte } // NewBoltResolver returns a new resolver based on BoltDB func NewBoltResolver() Repository { - return &boltResolver{ client: internal.GetBoltDb(), - bucketName: "address", + bucketName: []byte("address"), } } @@ -45,7 +45,7 @@ func (b boltResolver) Get(hash string) (*ResolveInfoType, error) { rec := &ResolveInfoType{} err := b.client.View(func(tx *bolt.Tx) error { - bucket := tx.Bucket([]byte(b.bucketName)) + bucket := tx.Bucket(b.bucketName) if bucket == nil { return ErrNotFound } @@ -65,9 +65,9 @@ func (b boltResolver) Get(hash string) (*ResolveInfoType, error) { return rec, nil } -func (b boltResolver) Create(hash, routing, publicKey, proof string) (bool, error) { +func (b boltResolver) Create(hash, routing string, publicKey *bmcrypto.PubKey, proof string) (bool, error) { err := b.client.Update(func(tx *bolt.Tx) error { - bucket, err := tx.CreateBucketIfNotExists([]byte(b.bucketName)) + bucket, err := tx.CreateBucketIfNotExists(b.bucketName) if err != nil { return err } @@ -75,15 +75,111 @@ func (b boltResolver) Create(hash, routing, publicKey, proof string) (bool, erro rec := &ResolveInfoType{ Hash: hash, RoutingID: routing, - PubKey: publicKey, + PubKey: publicKey.String(), Proof: proof, Serial: uint64(time.Now().UnixNano()), + Deleted: false, + DeletedAt: time.Time{}, } buf, err := json.Marshal(rec) if err != nil { return err } + err = bucket.Put([]byte(hash), buf) + if err != nil { + return err + } + + // Store in history + bucket, err = tx.CreateBucketIfNotExists([]byte(hash + "fingerprints")) + if err != nil { + return err + } + + b, err := json.Marshal(KSNormal) + if err != nil { + return err + } + return bucket.Put([]byte(publicKey.Fingerprint()), b) + }) + + if err != nil { + return false, err + } + + return true, nil +} + +func (b boltResolver) Update(info *ResolveInfoType, routing string, publicKey *bmcrypto.PubKey) (bool, error) { + err := b.client.Update(func(tx *bolt.Tx) error { + bucket := tx.Bucket(b.bucketName) + if bucket == nil { + return nil + } + + rec, err := getFromBucket(bucket, info.Hash) + if err != nil { + return ErrNotFound + } + + if rec.Serial != info.Serial { + return ErrNotFound + } + + rec.RoutingID = routing + rec.PubKey = publicKey.String() + buf, err := json.Marshal(rec) + if err != nil { + return err + } + + err = bucket.Put([]byte(info.Hash), buf) + if err != nil { + return err + } + + // Store in history (overwrite if already exists) + bucket, err = tx.CreateBucketIfNotExists([]byte(info.Hash + "fingerprints")) + if err != nil { + return err + } + + b, err := json.Marshal(KSNormal) + if err != nil { + return err + } + return bucket.Put([]byte(publicKey.Fingerprint()), b) + }) + + if err != nil { + return false, err + } + + return true, nil +} + +func (b boltResolver) SoftDelete(hash string) (bool, error) { + err := b.client.Update(func(tx *bolt.Tx) error { + bucket := tx.Bucket(b.bucketName) + if bucket == nil { + return nil + } + + rec, err := getFromBucket(bucket, hash) + if err != nil { + return ErrNotFound + } + + // make record deleted + rec.Deleted = true + rec.DeletedAt = time.Now() + + // Store + buf, err := json.Marshal(rec) + if err != nil { + return err + } return bucket.Put([]byte(hash), buf) }) @@ -94,13 +190,40 @@ func (b boltResolver) Create(hash, routing, publicKey, proof string) (bool, erro return true, nil } -func (b boltResolver) Update(info *ResolveInfoType, routing, publicKey string) (bool, error) { - return b.Create(info.Hash, routing, publicKey, info.Proof) +func (b boltResolver) SoftUndelete(hash string) (bool, error) { + err := b.client.Update(func(tx *bolt.Tx) error { + bucket := tx.Bucket(b.bucketName) + if bucket == nil { + return nil + } + + rec, err := getFromBucket(bucket, hash) + if err != nil { + return ErrNotFound + } + + // undelete + rec.Deleted = false + rec.DeletedAt = time.Time{} + + // Store + buf, err := json.Marshal(rec) + if err != nil { + return err + } + return bucket.Put([]byte(hash), buf) + }) + + if err != nil { + return false, err + } + + return true, nil } func (b boltResolver) Delete(hash string) (bool, error) { err := b.client.Update(func(tx *bolt.Tx) error { - bucket := tx.Bucket([]byte(b.bucketName)) + bucket := tx.Bucket(b.bucketName) if bucket == nil { return nil } @@ -114,3 +237,65 @@ func (b boltResolver) Delete(hash string) (bool, error) { return true, nil } + +func (b boltResolver) GetKeyStatus(hash string, fingerprint string) (KeyStatus, error) { + var ks KeyStatus + + err := b.client.View(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(hash + "fingerprints")) + if bucket == nil { + return ErrNotFound + } + + result := bucket.Get([]byte(fingerprint)) + if result == nil { + return ErrNotFound + } + + err := json.Unmarshal(result, &ks) + if err != nil { + return err + } + + return nil + }) + + return ks, err +} + +func (b boltResolver) SetKeyStatus(hash string, fingerprint string, status KeyStatus) error { + return b.client.Update(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(hash + "fingerprints")) + if bucket == nil { + return nil + } + + // Check if hash+fingerprint exist + result := bucket.Get([]byte(fingerprint)) + if result == nil { + return ErrNotFound + } + + b, err := json.Marshal(status) + if err != nil { + return err + } + + return bucket.Put([]byte(fingerprint), b) + }) +} + +func getFromBucket(bucket *bolt.Bucket, hash string) (*ResolveInfoType, error) { + data := bucket.Get([]byte(hash)) + if data == nil { + return nil, ErrNotFound + } + + rec := &ResolveInfoType{} + err := json.Unmarshal(data, &rec) + if err != nil { + return nil, ErrNotFound + } + + return rec, nil +} diff --git a/internal/address/bolt_test.go b/internal/address/bolt_test.go new file mode 100644 index 0000000..47efa49 --- /dev/null +++ b/internal/address/bolt_test.go @@ -0,0 +1,56 @@ +// Copyright (c) 2020 BitMaelum Authors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package address + +import ( + "fmt" + "math/rand" + "os" + "testing" +) + +const tmpDbPath = "/tmp/mockboltdb-%d.db" + +func TestBoltResolver(t *testing.T) { + // Random path, otherwise we get into issues with running on github actions? + p := fmt.Sprintf(tmpDbPath, rand.Int63()) + + _ = os.Setenv("USE_BOLT", "1") + _ = os.Setenv("BOLT_DB_FILE", p) + SetDefaultRepository(nil) + + _ = os.Remove(p) + db := NewBoltResolver() + runRepositoryCreateUpdateTest(t, db) + + _ = os.Remove(p) + db = NewBoltResolver() + runRepositoryDeletionTests(t, db) + + _ = os.Remove(p) + db = NewBoltResolver() + runRepositoryHistoryCheck(t, db) + + _ = os.Remove(p) + db = NewBoltResolver() + runRepositoryHistoryKeyStatus(t, db) + + _ = os.Remove(p) +} diff --git a/internal/address/dynamodb.go b/internal/address/dynamodb.go index d1e2c5e..aad2923 100644 --- a/internal/address/dynamodb.go +++ b/internal/address/dynamodb.go @@ -29,40 +29,54 @@ import ( "github.com/aws/aws-sdk-go/service/dynamodb" "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute" "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface" + "github.com/bitmaelum/bitmaelum-suite/pkg/bmcrypto" ) type dynamoDbResolver struct { - Dyna dynamodbiface.DynamoDBAPI - TableName string + Dyna dynamodbiface.DynamoDBAPI + TableName string + HistoryTableName string } -// ErrNotFound will be returned when a record we are looking for is not found in the db -var ErrNotFound = errors.New("record not found") +// Error codes +var ( + ErrNotFound = errors.New("record not found") + ErrCannotUpdate = errors.New("cannot update record") +) -// Record holds a DynamoDB record -type Record struct { +// record holds a DynamoDB record +type recordType struct { Hash string `dynamodbav:"hash"` Routing string `dynamodbav:"routing"` PublicKey string `dynamodbav:"public_key"` Proof string `dynamodbav:"proof"` Serial uint64 `dynamodbav:"sn"` + Deleted bool `dynamodbav:"deleted"` + DeletedAt uint64 `dynamodbav:"deleted_at"` +} + +type historyRecordType struct { + Hash string `dynamodbav:"hash"` + Fingerprint string `dynamodbav:"fingerprint"` + Status KeyStatus `dynamodbav:"status"` } // NewDynamoDBResolver returns a new resolver based on DynamoDB -func NewDynamoDBResolver(client dynamodbiface.DynamoDBAPI, tableName string) Repository { +func NewDynamoDBResolver(client dynamodbiface.DynamoDBAPI, tableName, historyTableName string) Repository { return &dynamoDbResolver{ - Dyna: client, - TableName: tableName, + Dyna: client, + TableName: tableName, + HistoryTableName: historyTableName, } } -func (r *dynamoDbResolver) Update(info *ResolveInfoType, routing, publicKey string) (bool, error) { +func (r *dynamoDbResolver) Update(info *ResolveInfoType, routing string, publicKey *bmcrypto.PubKey) (bool, error) { serial := strconv.FormatUint(uint64(time.Now().UnixNano()), 10) input := &dynamodb.UpdateItemInput{ ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{ ":s": {S: aws.String(routing)}, - ":pk": {S: aws.String(publicKey)}, + ":pk": {S: aws.String(publicKey.String())}, ":sn": {N: aws.String(serial)}, ":csn": {N: aws.String(strconv.FormatUint(info.Serial, 10))}, }, @@ -74,7 +88,14 @@ func (r *dynamoDbResolver) Update(info *ResolveInfoType, routing, publicKey stri }, } - _, err := r.Dyna.UpdateItem(input) + // Update key history + _, err := r.updateKeyHistory(info.Hash, publicKey.Fingerprint(), KSNormal) + if err != nil { + return false, err + } + + // Update address record + _, err = r.Dyna.UpdateItem(input) if err != nil { log.Print(err) return false, err @@ -83,11 +104,11 @@ func (r *dynamoDbResolver) Update(info *ResolveInfoType, routing, publicKey stri return true, nil } -func (r *dynamoDbResolver) Create(hash, routing, publicKey, proof string) (bool, error) { - record := Record{ +func (r *dynamoDbResolver) Create(hash, routing string, publicKey *bmcrypto.PubKey, proof string) (bool, error) { + record := recordType{ Hash: hash, Routing: routing, - PublicKey: publicKey, + PublicKey: publicKey.String(), Proof: proof, Serial: uint64(TimeNow().UnixNano()), } @@ -103,6 +124,13 @@ func (r *dynamoDbResolver) Create(hash, routing, publicKey, proof string) (bool, TableName: aws.String(r.TableName), } + // Update key history + _, err = r.updateKeyHistory(hash, publicKey.Fingerprint(), KSNormal) + if err != nil { + return false, err + } + + // Create address record _, err = r.Dyna.PutItem(input) return err == nil, err } @@ -125,13 +153,18 @@ func (r *dynamoDbResolver) Get(hash string) (*ResolveInfoType, error) { return nil, ErrNotFound } - record := Record{} + record := recordType{} err = dynamodbattribute.UnmarshalMap(result.Item, &record) if err != nil { log.Print(err) return nil, ErrNotFound } + // We would prefer if we didn't retrieve it from the Getitem input + if record.Deleted { + return nil, ErrNotFound + } + return &ResolveInfoType{ Hash: record.Hash, RoutingID: record.Routing, @@ -157,3 +190,111 @@ func (r *dynamoDbResolver) Delete(hash string) (bool, error) { return true, nil } + +func (r *dynamoDbResolver) SoftDelete(hash string) (bool, error) { + input := &dynamodb.UpdateItemInput{ + ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{ + ":dt": {N: aws.String(strconv.FormatInt(time.Now().Unix(), 10))}, + }, + TableName: aws.String(r.TableName), + UpdateExpression: aws.String("SET deleted=1, deleted_at=:dt"), + Key: map[string]*dynamodb.AttributeValue{ + "hash": {S: aws.String(hash)}, + }, + } + + _, err := r.Dyna.UpdateItem(input) + if err != nil { + log.Print(err) + return false, err + } + + return true, nil +} + +func (r *dynamoDbResolver) SoftUndelete(hash string) (bool, error) { + input := &dynamodb.UpdateItemInput{ + ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{ + ":dt": {N: aws.String("")}, + }, + TableName: aws.String(r.TableName), + UpdateExpression: aws.String("SET deleted=0, deleted_at=:dt"), + Key: map[string]*dynamodb.AttributeValue{ + "hash": {S: aws.String(hash)}, + }, + } + + _, err := r.Dyna.UpdateItem(input) + if err != nil { + log.Print(err) + return false, err + } + + return true, nil +} + +func (r *dynamoDbResolver) GetKeyStatus(hash string, fingerprint string) (KeyStatus, error) { + result, err := r.Dyna.GetItem(&dynamodb.GetItemInput{ + TableName: aws.String(r.HistoryTableName), + Key: map[string]*dynamodb.AttributeValue{ + "hash": {S: aws.String(hash)}, + "fingerprint": {S: aws.String(fingerprint)}, + }, + }) + // Error while fetching record + if err != nil { + return KSNormal, err + } + + // Item not found + if result.Item == nil { + return KSNormal, ErrNotFound + } + + record := historyRecordType{} + err = dynamodbattribute.UnmarshalMap(result.Item, &record) + if err != nil { + log.Print(err) + return KSNormal, ErrNotFound + } + + return record.Status, nil +} + +func (r *dynamoDbResolver) SetKeyStatus(hash string, fingerprint string, status KeyStatus) error { + // Make sure key exists before updating + _, err := r.GetKeyStatus(hash, fingerprint) + if err != nil { + return err + } + + ok, err := r.updateKeyHistory(hash, fingerprint, status) + if err != nil { + return err + } + + if !ok { + return ErrCannotUpdate + } + + return nil +} + +func (r *dynamoDbResolver) updateKeyHistory(hash, fingerprint string, status KeyStatus) (bool, error) { + av, err := dynamodbattribute.MarshalMap(historyRecordType{ + Hash: hash, + Fingerprint: fingerprint, + Status: status, + }) + if err != nil { + return false, err + } + + input := &dynamodb.PutItemInput{ + Item: av, + TableName: aws.String(r.HistoryTableName), + } + + _, err = r.Dyna.PutItem(input) + return err == nil, err +} diff --git a/internal/address/dynamodb_test.go b/internal/address/dynamodb_test.go index 4cb2515..8150de1 100644 --- a/internal/address/dynamodb_test.go +++ b/internal/address/dynamodb_test.go @@ -20,12 +20,15 @@ package address import ( + "fmt" "testing" "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/dynamodb" "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface" + "github.com/bitmaelum/bitmaelum-suite/pkg/bmcrypto" + "github.com/bitmaelum/bitmaelum-suite/pkg/hash" dynamock "github.com/gusaul/go-dynamock" "github.com/stretchr/testify/assert" ) @@ -37,7 +40,7 @@ var ( func TestGet(t *testing.T) { var client dynamodbiface.DynamoDBAPI client, mock = dynamock.New() - resolver := NewDynamoDBResolver(client, "mock_address_table") + resolver := NewDynamoDBResolver(client, "mock_address_table", "mock_history_table") ri, err := resolver.Get("cf99b895f350b77585881438ab38a935e68c9c7409c5adaad23fb17572ca1ea2") assert.Error(t, err) @@ -86,7 +89,7 @@ func TestGet(t *testing.T) { func TestDelete(t *testing.T) { var client dynamodbiface.DynamoDBAPI client, mock = dynamock.New() - resolver := NewDynamoDBResolver(client, "mock_address_table") + resolver := NewDynamoDBResolver(client, "mock_address_table", "mock_history_table") // No record found result := dynamodb.DeleteItemOutput{ @@ -106,13 +109,18 @@ func TestDelete(t *testing.T) { func TestCreate(t *testing.T) { var client dynamodbiface.DynamoDBAPI client, mock = dynamock.New() - resolver := NewDynamoDBResolver(client, "mock_address_table") + resolver := NewDynamoDBResolver(client, "mock_address_table", "mock_history_table") TimeNow = func() time.Time { return time.Date(2010, 05, 10, 12, 34, 56, 0, time.UTC) } - result := dynamodb.PutItemOutput{} + historyItems := map[string]*dynamodb.AttributeValue{ + "hash": {S: aws.String("cf99b895f350b77585881438ab38a935e68c9c7409c5adaad23fb17572ca1ea2")}, + "fingerprint": {S: aws.String("b74bb232a9ea0154c10f275da4be8a4233fcf7c3bc42038206fe527cb566f758")}, + "status": {N: aws.String(fmt.Sprintf("%d", KSNormal))}, + } + mock.ExpectPutItem().ToTable("mock_history_table").WithItems(historyItems).WillReturns(dynamodb.PutItemOutput{}) items := map[string]*dynamodb.AttributeValue{ "hash": {S: aws.String("cf99b895f350b77585881438ab38a935e68c9c7409c5adaad23fb17572ca1ea2")}, @@ -120,9 +128,13 @@ func TestCreate(t *testing.T) { "public_key": {S: aws.String("ed25519 MCowBQYDK2VwAyEAS2/hs2jf0QJgpuNklMnN/A7EHj26DDpRfvcZyettOjU=")}, "routing": {S: aws.String("12345678")}, "sn": {N: aws.String("1273494896000000000")}, + "deleted_at": {N: aws.String("0")}, + "deleted": {BOOL: aws.Bool(false)}, } - mock.ExpectPutItem().ToTable("mock_address_table").WithItems(items).WillReturns(result) - ok, err := resolver.Create("cf99b895f350b77585881438ab38a935e68c9c7409c5adaad23fb17572ca1ea2", "12345678", "ed25519 MCowBQYDK2VwAyEAS2/hs2jf0QJgpuNklMnN/A7EHj26DDpRfvcZyettOjU=", "proof") + mock.ExpectPutItem().ToTable("mock_address_table").WithItems(items).WillReturns(dynamodb.PutItemOutput{}) + + pubkey, _ := bmcrypto.NewPubKey("ed25519 MCowBQYDK2VwAyEAS2/hs2jf0QJgpuNklMnN/A7EHj26DDpRfvcZyettOjU=") + ok, err := resolver.Create("cf99b895f350b77585881438ab38a935e68c9c7409c5adaad23fb17572ca1ea2", "12345678", pubkey, "proof") assert.NoError(t, err) assert.True(t, ok) } @@ -130,7 +142,9 @@ func TestCreate(t *testing.T) { func TestUpdate(t *testing.T) { var client dynamodbiface.DynamoDBAPI client, mock = dynamock.New() - resolver := NewDynamoDBResolver(client, "mock_address_table") + resolver := NewDynamoDBResolver(client, "mock_address_table", "mock_history_table") + + pubkey, _ := bmcrypto.NewPubKey("ed25519 MCowBQYDK2VwAyEAS2/hs2jf0QJgpuNklMnN/A7EHj26DDpRfvcZyettOjU=") expectKey := map[string]*dynamodb.AttributeValue{ "hash": { @@ -139,18 +153,75 @@ func TestUpdate(t *testing.T) { } mock.ExpectUpdateItem().ToTable("mock_address_table").WithKeys(expectKey) + expectedItems := map[string]*dynamodb.AttributeValue{ + "hash": {S: aws.String("cf99b895f350b77585881438ab38a935e68c9c7409c5adaad23fb17572ca1ea2")}, + "fingerprint": {S: aws.String(pubkey.Fingerprint())}, + "status": {N: aws.String(fmt.Sprintf("%d", KSNormal))}, + } + mock.ExpectPutItem().ToTable("mock_history_table").WithItems(expectedItems) + info := &ResolveInfoType{ Hash: "cf99b895f350b77585881438ab38a935e68c9c7409c5adaad23fb17572ca1ea2", RoutingID: "12345678", - PubKey: "ed25519 MCowBQYDK2VwAyEAS2/hs2jf0QJgpuNklMnN/A7EHj26DDpRfvcZyettOjU=", + PubKey: pubkey.String(), Proof: "proof", Serial: 1273494896000000000, } - ok, err := resolver.Update(info, "555555555", "pubkey222") + ok, err := resolver.Update(info, "555555555", pubkey) assert.NoError(t, err) assert.True(t, ok) } +func TestHistory(t *testing.T) { + var client dynamodbiface.DynamoDBAPI + client, mock = dynamock.New() + resolver := NewDynamoDBResolver(client, "mock_address_table", "mock_history_table") + + addrHash := hash.Hash("addr1") + + _, pub1, _ := bmcrypto.GenerateKeyPair(bmcrypto.KeyTypeED25519) + + // Cannot set key status when it doesn't exist yet + err := resolver.SetKeyStatus(addrHash.String(), pub1.Fingerprint(), KSNormal) + assert.Error(t, err) + + // Set first key to normal + + expectedItems := map[string]*dynamodb.AttributeValue{ + "hash": {S: aws.String(addrHash.String())}, + "fingerprint": {S: aws.String(pub1.Fingerprint())}, + "status": {N: aws.String(fmt.Sprintf("%d", KSNormal))}, + } + mock.ExpectPutItem().ToTable("mock_history_table").WithItems(expectedItems) + + result := dynamodb.GetItemOutput{ + Item: map[string]*dynamodb.AttributeValue{ + "hash": {S: aws.String(addrHash.String())}, + "fingerprint": {S: aws.String(pub1.Fingerprint())}, + "status": {N: aws.String("1")}, + }, + } + mock.ExpectGetItem().ToTable("mock_history_table").WillReturns(result) + + err = resolver.SetKeyStatus(addrHash.String(), pub1.Fingerprint(), KSNormal) + assert.NoError(t, err) + + // Set second key to compromised + _, pub2, _ := bmcrypto.GenerateKeyPair(bmcrypto.KeyTypeED25519) + + expectedItems = map[string]*dynamodb.AttributeValue{ + "hash": {S: aws.String(addrHash.String())}, + "fingerprint": {S: aws.String(pub2.Fingerprint())}, + "status": {N: aws.String(fmt.Sprintf("%d", KSCompromised))}, + } + mock.ExpectPutItem().ToTable("mock_history_table").WithItems(expectedItems) + + mock.ExpectGetItem().ToTable("mock_history_table").WillReturns(result) + + err = resolver.SetKeyStatus(addrHash.String(), pub2.Fingerprint(), KSCompromised) + assert.NoError(t, err) +} + func TestResolver(t *testing.T) { r := GetResolveRepository() assert.NotNil(t, r) diff --git a/internal/address/repository.go b/internal/address/repository.go index 04a9be5..e91dce1 100644 --- a/internal/address/repository.go +++ b/internal/address/repository.go @@ -20,10 +20,13 @@ package address import ( + "errors" "os" + "time" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/dynamodb" + "github.com/bitmaelum/bitmaelum-suite/pkg/bmcrypto" ) // ResolveInfoType returns information found in the resolver repository @@ -33,14 +36,55 @@ type ResolveInfoType struct { PubKey string Proof string Serial uint64 + Deleted bool + DeletedAt time.Time +} + +type KeyStatus int + +const ( + KSNormal KeyStatus = iota + 1 // Regular key, just rotated + KSCompromised // Key was compromised +) + +var keyStatusMap = map[KeyStatus]string{ + KSNormal: "normal", + KSCompromised: "compromised", +} + +func (k KeyStatus) ToString() string { + return keyStatusMap[k] +} + +func StringToKeyStatus(s string) (KeyStatus, error) { + for i := range keyStatusMap { + if keyStatusMap[i] == s { + return i, nil + } + } + + return 0, errors.New("keystatus not found") } // Repository to resolve records type Repository interface { + // Retrieve from hash Get(hash string) (*ResolveInfoType, error) - Create(hash, routing, publicKey, proof string) (bool, error) - Update(info *ResolveInfoType, routing, publicKey string) (bool, error) + // Create a new entry + Create(hash, routing string, publicKey *bmcrypto.PubKey, proof string) (bool, error) + // Update an existing entry + Update(info *ResolveInfoType, routing string, publicKey *bmcrypto.PubKey) (bool, error) + // Softdelete an entry + SoftDelete(hash string) (bool, error) + // Undelete a softdeleted entry + SoftUndelete(hash string) (bool, error) + // Remove the entry completely (destructive) Delete(hash string) (bool, error) + + // Get the status of this (old) key + GetKeyStatus(hash string, fingerprint string) (KeyStatus, error) + // Set the given key status + SetKeyStatus(hash string, fingerprint string, status KeyStatus) error } var resolver Repository @@ -60,7 +104,7 @@ func GetResolveRepository() Repository { SharedConfigState: session.SharedConfigEnable, })) - resolver = NewDynamoDBResolver(dynamodb.New(sess), os.Getenv("ADDRESS_TABLE_NAME")) + resolver = NewDynamoDBResolver(dynamodb.New(sess), os.Getenv("ADDRESS_TABLE_NAME"), os.Getenv("HISTORY_TABLE_NAME")) return resolver } diff --git a/internal/address/repository_test.go b/internal/address/repository_test.go index 4352f1d..c116edf 100644 --- a/internal/address/repository_test.go +++ b/internal/address/repository_test.go @@ -22,23 +22,280 @@ package address import ( "os" + "github.com/bitmaelum/bitmaelum-suite/pkg/bmcrypto" + "github.com/bitmaelum/bitmaelum-suite/pkg/hash" + testing2 "github.com/bitmaelum/key-resolver-go/internal/testing" "github.com/stretchr/testify/assert" "testing" ) func TestDynamoRepo(t *testing.T) { + _ = os.Setenv("USE_BOLT", "0") _ = os.Setenv("ADDRESS_TABLE_NAME", "mock") + SetDefaultRepository(nil) r := GetResolveRepository() - assert.IsType(t, r, NewDynamoDBResolver(nil, "")) + assert.IsType(t, r, NewDynamoDBResolver(nil, "", "")) } -func TestBoltResolver(t *testing.T) { +func TestBoltResolverRepo(t *testing.T) { _ = os.Setenv("USE_BOLT", "1") - _ = os.Setenv("BOLT_DB_FILE", "./mockdb.db") + _ = os.Setenv("BOLT_DB_FILE", "/tmp/mockdb.db") SetDefaultRepository(nil) r := GetResolveRepository() assert.IsType(t, r, NewBoltResolver()) } + +func runRepositoryHistoryCheck(t *testing.T, db Repository) { + h1 := hash.Hash("address1!") + h2 := hash.Hash("address2!") + + _, pub1, _ := testing2.ReadTestKey("../../testdata/key-1.json") + _, pub2, _ := testing2.ReadTestKey("../../testdata/key-2.json") + _, pub3, _ := testing2.ReadTestKey("../../testdata/key-3.json") + _, pub4, _ := testing2.ReadTestKey("../../testdata/key-4.json") + + ok, err := db.Create(h1.String(), "12345678", pub1, "proof") + assert.NoError(t, err) + assert.True(t, ok) + + // Correct key + res, err := db.GetKeyStatus(h1.String(), pub1.Fingerprint()) + assert.NoError(t, err) + assert.Equal(t, KSNormal, res) + + // Other key + _, err = db.GetKeyStatus(h1.String(), pub2.Fingerprint()) + assert.Error(t, err) + + // Other account + _, err = db.GetKeyStatus(h2.String(), pub1.Fingerprint()) + assert.Error(t, err) + + // update key + info, _ := db.Get(h1.String()) + ok, err = db.Update(info, "12345678", pub2) + assert.NoError(t, err) + assert.True(t, ok) + + // Correct key + res, err = db.GetKeyStatus(h1.String(), pub1.Fingerprint()) + assert.NoError(t, err) + assert.Equal(t, KSNormal, res) + + // Other key is correct as well now + res, err = db.GetKeyStatus(h1.String(), pub2.Fingerprint()) + assert.NoError(t, err) + assert.Equal(t, KSNormal, res) + + // Other account still not the key + _, err = db.GetKeyStatus(h2.String(), pub1.Fingerprint()) + assert.Error(t, err) + + // update key on other account + ok, err = db.Create(h2.String(), "12345678", pub3, "proof") + assert.NoError(t, err) + assert.True(t, ok) + + // Correct key + res, err = db.GetKeyStatus(h1.String(), pub1.Fingerprint()) + assert.NoError(t, err) + assert.Equal(t, KSNormal, res) + + // Other key is correct as well now + res, err = db.GetKeyStatus(h1.String(), pub2.Fingerprint()) + assert.NoError(t, err) + assert.Equal(t, KSNormal, res) + + // Other account still not the key + _, err = db.GetKeyStatus(h2.String(), pub1.Fingerprint()) + assert.Error(t, err) + + // Other account has other key + res, err = db.GetKeyStatus(h2.String(), pub3.Fingerprint()) + assert.NoError(t, err) + assert.Equal(t, KSNormal, res) + + // Update first key again + info, _ = db.Get(h1.String()) + ok, err = db.Update(info, "12345678", pub4) + assert.NoError(t, err) + assert.True(t, ok) + + // Correct key + res, err = db.GetKeyStatus(h1.String(), pub1.Fingerprint()) + assert.NoError(t, err) + assert.Equal(t, KSNormal, res) + + // Other key is correct as well now + res, err = db.GetKeyStatus(h1.String(), pub2.Fingerprint()) + assert.NoError(t, err) + assert.Equal(t, KSNormal, res) + + // Pub3 is not here + _, err = db.GetKeyStatus(h1.String(), pub3.Fingerprint()) + assert.Error(t, err) + + res, err = db.GetKeyStatus(h1.String(), pub4.Fingerprint()) + assert.NoError(t, err) + assert.Equal(t, KSNormal, res) +} + +func runRepositoryHistoryKeyStatus(t *testing.T, db Repository) { + h1 := hash.Hash("address1!") + + _, pub1, _ := testing2.ReadTestKey("../../testdata/key-1.json") + _, pub2, _ := testing2.ReadTestKey("../../testdata/key-2.json") + + ok, err := db.Create(h1.String(), "12345678", pub1, "proof") + assert.NoError(t, err) + assert.True(t, ok) + + // Correct key in normal state + res, err := db.GetKeyStatus(h1.String(), pub1.Fingerprint()) + assert.NoError(t, err) + assert.Equal(t, KSNormal, res) + + // Rotate key + info, _ := db.Get(h1.String()) + ok, err = db.Update(info, "12345678", pub2) + assert.NoError(t, err) + assert.True(t, ok) + + // Set compromised key status of key 1 + err = db.SetKeyStatus(h1.String(), pub1.Fingerprint(), KSCompromised) + assert.NoError(t, err) + + // First key is compromised + res, err = db.GetKeyStatus(h1.String(), pub1.Fingerprint()) + assert.NoError(t, err) + assert.Equal(t, KSCompromised, res) + + // Second key is normal + res, err = db.GetKeyStatus(h1.String(), pub2.Fingerprint()) + assert.NoError(t, err) + assert.Equal(t, KSNormal, res) + +} + +func runRepositoryCreateUpdateTest(t *testing.T, db Repository) { + h1 := hash.Hash("address1!") + h2 := hash.Hash("address2!") + + _, pubkey, _ := bmcrypto.GenerateKeyPair("ed25519") + + // Create key + ok, err := db.Create(h1.String(), "12345678", pubkey, "proof") + assert.NoError(t, err) + assert.True(t, ok) + + // Fetch unknown hash + info, err := db.Get(h2.String()) + assert.Error(t, err) + assert.Nil(t, info) + + // Fetch created hash + info, err = db.Get(h1.String()) + assert.NoError(t, err) + assert.Equal(t, "12345678", info.RoutingID) + assert.Equal(t, h1.String(), info.Hash) + assert.Equal(t, pubkey.String(), info.PubKey) + assert.Equal(t, "proof", info.Proof) + + _, pubkey2, _ := bmcrypto.GenerateKeyPair("ed25519") + + // Update info + ok, err = db.Update(info, "11112222", pubkey2) + assert.NoError(t, err) + assert.True(t, ok) + + // Fetch info + info, err = db.Get(h1.String()) + assert.NoError(t, err) + assert.Equal(t, "11112222", info.RoutingID) + assert.Equal(t, h1.String(), info.Hash) + assert.Equal(t, pubkey2.String(), info.PubKey) + assert.Equal(t, "proof", info.Proof) + + _, pubkey3, _ := bmcrypto.GenerateKeyPair("ed25519") + + // Try and update with incorrect serial number + info.Serial = 1234 + ok, err = db.Update(info, "88881111", pubkey3) + assert.False(t, ok) + assert.Error(t, err) + + // Read back unmodified info + info, err = db.Get(h1.String()) + assert.NoError(t, err) + assert.Equal(t, "11112222", info.RoutingID) + assert.Equal(t, h1.String(), info.Hash) + assert.Equal(t, pubkey2.String(), info.PubKey) + assert.Equal(t, "proof", info.Proof) +} + +func runRepositoryDeletionTests(t *testing.T, db Repository) { + h1 := hash.Hash("address1!") + h2 := hash.Hash("address2!") + + _, pubkey, _ := bmcrypto.GenerateKeyPair("ed25519") + + // Create key + ok, err := db.Create(h1.String(), "12345678", pubkey, "proof") + assert.NoError(t, err) + assert.True(t, ok) + + // Fetch created hash + info, err := db.Get(h1.String()) + assert.NoError(t, err) + assert.NotNil(t, info) + + // Try and softdelete unknown + ok, err = db.SoftDelete(h2.String()) + assert.Error(t, err) + assert.False(t, ok) + + // Softdelete known entry + ok, err = db.SoftDelete(h1.String()) + assert.NoError(t, err) + assert.True(t, ok) + + info, err = db.Get(h1.String()) + assert.NoError(t, err) + assert.NotNil(t, info) + + // Softdelete again + ok, err = db.SoftDelete(h1.String()) + assert.NoError(t, err) + assert.True(t, ok) + + // Undelete unknown + ok, err = db.SoftUndelete(h2.String()) + assert.Error(t, err) + assert.False(t, ok) + + // undelete known + ok, err = db.SoftUndelete(h1.String()) + assert.NoError(t, err) + assert.True(t, ok) + + info, err = db.Get(h1.String()) + assert.NoError(t, err) + assert.Equal(t, h1.String(), info.Hash) + + // permanently delete known + ok, err = db.Delete(h1.String()) + assert.NoError(t, err) + assert.True(t, ok) + + // cannot undelete + ok, err = db.SoftUndelete(h1.String()) + assert.Error(t, err) + assert.False(t, ok) + + info, err = db.Get(h1.String()) + assert.Error(t, err) + assert.Nil(t, info) +} diff --git a/internal/address/sqlite.go b/internal/address/sqlite.go index 793231f..b643e68 100644 --- a/internal/address/sqlite.go +++ b/internal/address/sqlite.go @@ -20,6 +20,7 @@ package address import ( + "errors" "fmt" "strconv" "strings" @@ -27,6 +28,7 @@ import ( "database/sql" + "github.com/bitmaelum/bitmaelum-suite/pkg/bmcrypto" _ "github.com/mattn/go-sqlite3" // SQLite driver ) @@ -38,7 +40,7 @@ type SqliteDbResolver struct { } // NewDynamoDBResolver returns a new resolver based on DynamoDB -func NewSqliteResolver(dsn string) *SqliteDbResolver { +func NewSqliteResolver(dsn string) Repository { if !strings.HasPrefix(dsn, "file:") { if dsn == ":memory:" { dsn = "file::memory:?mode=memory" @@ -58,7 +60,12 @@ func NewSqliteResolver(dsn string) *SqliteDbResolver { TimeNow: time.Now(), } - _, err = db.conn.Exec("CREATE TABLE IF NOT EXISTS mock_address (hash VARCHAR(64) PRIMARY KEY, pubkey TEXT, routing_id VARCHAR(64), proof TEXT, serial INT)") + _, err = db.conn.Exec("CREATE TABLE IF NOT EXISTS mock_address (hash VARCHAR(64) PRIMARY KEY, pubkey TEXT, routing_id VARCHAR(64), proof TEXT, serial INTEGER, deleted INTEGER, deleted_at INTEGER)") + if err != nil { + return nil + } + + _, err = db.conn.Exec("CREATE TABLE IF NOT EXISTS mock_history (hash VARCHAR(64), fingerprint VARCHAR(64), status INTEGER, PRIMARY KEY (hash, fingerprint))") if err != nil { return nil } @@ -66,33 +73,49 @@ func NewSqliteResolver(dsn string) *SqliteDbResolver { return db } -func (r *SqliteDbResolver) Update(info *ResolveInfoType, routing, publicKey string) (bool, error) { +func (r *SqliteDbResolver) Update(info *ResolveInfoType, routing string, publicKey *bmcrypto.PubKey) (bool, error) { newSerial := strconv.FormatUint(uint64(r.TimeNow.UnixNano()), 10) + _ = r.updateKeyHistory(info.Hash, publicKey.Fingerprint(), KSNormal) + st, err := r.conn.Prepare("UPDATE mock_address SET routing_id=?, pubkey=?, serial=? WHERE hash=? AND serial=?") if err != nil { return false, err } - res, err := st.Exec(routing, publicKey, newSerial, info.Hash, info.Serial) + res, err := st.Exec(routing, publicKey.String(), newSerial, info.Hash, info.Serial) if err != nil { return false, err } count, err := res.RowsAffected() - return count != 0, err + if err != nil { + return false, err + } + if count == 0 { + return false, errors.New("not updated") + } + return true, nil } -func (r *SqliteDbResolver) Create(hash, routing, publicKey, proof string) (bool, error) { +func (r *SqliteDbResolver) Create(hash, routing string, publicKey *bmcrypto.PubKey, proof string) (bool, error) { serial := strconv.FormatUint(uint64(r.TimeNow.UnixNano()), 10) - res, err := r.conn.Exec("INSERT INTO mock_address VALUES (?, ?, ?, ?, ?)", hash, publicKey, routing, proof, serial) + _ = r.updateKeyHistory(hash, publicKey.Fingerprint(), KSNormal) + + res, err := r.conn.Exec("INSERT INTO mock_address VALUES (?, ?, ?, ?, ?, 0, 0)", hash, publicKey.String(), routing, proof, serial) if err != nil { return false, err } count, err := res.RowsAffected() - return count != 0, err + if err != nil { + return false, err + } + if count == 0 { + return false, errors.New("not created") + } + return true, nil } func (r *SqliteDbResolver) Get(hash string) (*ResolveInfoType, error) { @@ -102,9 +125,11 @@ func (r *SqliteDbResolver) Get(hash string) (*ResolveInfoType, error) { rt string pow string sn uint64 + d bool + da int64 ) - err := r.conn.QueryRow("SELECT hash, pubkey, routing_id, proof, serial FROM mock_address WHERE hash LIKE ?", hash).Scan(&h, &pk, &rt, &pow, &sn) + err := r.conn.QueryRow("SELECT hash, pubkey, routing_id, proof, serial, deleted, deleted_at FROM mock_address WHERE hash LIKE ?", hash).Scan(&h, &pk, &rt, &pow, &sn, &d, &da) if err != nil { return nil, ErrNotFound } @@ -115,6 +140,8 @@ func (r *SqliteDbResolver) Get(hash string) (*ResolveInfoType, error) { PubKey: pk, Proof: pow, Serial: sn, + Deleted: d, + DeletedAt: time.Unix(da, 0), }, nil } @@ -125,5 +152,83 @@ func (r *SqliteDbResolver) Delete(hash string) (bool, error) { } count, err := res.RowsAffected() - return count != 0, err + if err != nil { + return false, err + } + if count == 0 { + return false, errors.New("not deleted") + } + return true, nil +} + +func (r *SqliteDbResolver) SoftDelete(hash string) (bool, error) { + st, err := r.conn.Prepare("UPDATE mock_address SET deleted=1, deleted_at=? WHERE hash=?") + if err != nil { + return false, err + } + + dt := time.Now().Unix() + res, err := st.Exec(dt, hash) + if err != nil { + return false, err + } + + count, err := res.RowsAffected() + if err != nil { + return false, err + } + if count == 0 { + return false, errors.New("not soft deleted") + } + return true, nil +} + +func (r *SqliteDbResolver) SoftUndelete(hash string) (bool, error) { + st, err := r.conn.Prepare("UPDATE mock_address SET deleted=0, deleted_at=0 WHERE hash=?") + if err != nil { + return false, err + } + + res, err := st.Exec(hash) + if err != nil { + return false, err + } + + count, err := res.RowsAffected() + if err != nil { + return false, err + } + if count == 0 { + return false, errors.New("not soft undeleted") + } + return true, nil +} + +func (r *SqliteDbResolver) GetKeyStatus(hash string, fingerprint string) (KeyStatus, error) { + var ks *KeyStatus + + err := r.conn.QueryRow("SELECT status FROM mock_history WHERE hash LIKE ? AND fingerprint LIKE ?", hash, fingerprint).Scan(&ks) + if err != nil { + return KSNormal, ErrNotFound + } + + return *ks, nil +} + +func (r *SqliteDbResolver) updateKeyHistory(hash string, fingerprint string, status KeyStatus) error { + _, err := r.conn.Exec("INSERT OR REPLACE INTO mock_history VALUES (?, ?, ?)", hash, fingerprint, status) + + return err +} + +func (r *SqliteDbResolver) SetKeyStatus(hash string, fingerprint string, status KeyStatus) error { + var ks *KeyStatus + + // Make sure key exists before adding status + err := r.conn.QueryRow("SELECT status FROM mock_history WHERE hash LIKE ? AND fingerprint LIKE ?", hash, fingerprint).Scan(&ks) + if err != nil { + return ErrNotFound + } + + return r.updateKeyHistory(hash, fingerprint, status) } diff --git a/internal/address/sqlite_test.go b/internal/address/sqlite_test.go new file mode 100644 index 0000000..cbe6bae --- /dev/null +++ b/internal/address/sqlite_test.go @@ -0,0 +1,38 @@ +// Copyright (c) 2020 BitMaelum Authors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package address + +import ( + "testing" +) + +func TestSqliteDbResolver(t *testing.T) { + db := NewSqliteResolver(":memory:") + runRepositoryCreateUpdateTest(t, db) + + db = NewSqliteResolver(":memory:") + runRepositoryDeletionTests(t, db) + + db = NewSqliteResolver(":memory:") + runRepositoryHistoryCheck(t, db) + + db = NewSqliteResolver(":memory:") + runRepositoryHistoryKeyStatus(t, db) +} diff --git a/internal/address/token_test.go b/internal/address/token_test.go index 5afb9de..fe16403 100644 --- a/internal/address/token_test.go +++ b/internal/address/token_test.go @@ -20,9 +20,6 @@ package address import ( - "io/ioutil" - "log" - "os" "testing" "time" @@ -81,7 +78,11 @@ func TestToken(t *testing.T) { assert.False(t, ok) } -func TestMain(m *testing.M) { - log.SetOutput(ioutil.Discard) - os.Exit(m.Run()) +func TestGenerateToken(t *testing.T) { + h1 := hash.Hash("address1") + expires := time.Date(2010, 12, 31, 12, 34, 56, 0, time.UTC) + priv, _, _ := testing2.ReadTestKey("../../testdata/key-1.json") + + tok := GenerateToken(h1, "12345678", expires, *priv) + assert.Equal(t, "YWRkcmVzczE6MTIzNDU2Nzg6MTI5Mzc5ODg5NjrIdRSDpUD51Xmk+Yvfo8PI9DM/nsdJaT2I/nOikqrj/b+NdjWw7tZkEYj8/Vn63cnNdoaAO3xsFBbBClEOz+/Sfvm3JjLJ0aYVJ2IFXbRc2PxOY64ISw3xU+vYCPQLw/7goCN/2ktS5FW8qpuW8KkUepOl7hfVOHp45rJqtdtOypsvxyyPal1LxfGoVE1vg9VXPXbpQob7LS0nWUi6cKTbq2d1y3U92timd9CZofhcuX6q4J+nHuwYD1NVYz1ssDSs5wr8h/rpnCO08q3cJA24+erAsLjFjqMRCbk9wi3AogK1C3dPmrNZ8ZAAyFrMahp18qRQRirGLqdWfE5oczl6", tok) } diff --git a/internal/apigateway/request.go b/internal/apigateway/request.go index b1abece..1d49b1b 100644 --- a/internal/apigateway/request.go +++ b/internal/apigateway/request.go @@ -26,10 +26,12 @@ import ( // ReqToHTTP converts a api gateway http request to our internal http request func ReqToHTTP(req *events.APIGatewayV2HTTPRequest) *http.Request { + httpReq := http.NewRequest( req.RequestContext.HTTP.Method, req.RequestContext.HTTP.Path, req.Body, + req.PathParameters, ) // Add headers diff --git a/internal/apigateway/request_test.go b/internal/apigateway/request_test.go new file mode 100644 index 0000000..86fd723 --- /dev/null +++ b/internal/apigateway/request_test.go @@ -0,0 +1,80 @@ +// Copyright (c) 2020 BitMaelum Authors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package apigateway + +import ( + "testing" + + "github.com/aws/aws-lambda-go/events" + "github.com/bitmaelum/key-resolver-go/internal/http" + "github.com/stretchr/testify/assert" +) + +func TestReqToHTTP(t *testing.T) { + req := &events.APIGatewayV2HTTPRequest{ + Version: "latest", + RouteKey: "foo", + RawPath: "/foo", + Headers: map[string]string{ + "header-1": "value-1", + "header-2": "value-2", + }, + RequestContext: events.APIGatewayV2HTTPRequestContext{ + RouteKey: "foo", + AccountID: "1234", + Stage: "test", + HTTP: events.APIGatewayV2HTTPRequestContextHTTPDescription{ + Method: "GET", + Path: "/foobar", + Protocol: "https", + SourceIP: "127.2.3.4", + UserAgent: "gotest", + }, + }, + Body: "body", + } + + httpReq := ReqToHTTP(req) + assert.Equal(t, httpReq.Body, "body") + assert.Equal(t, httpReq.URL, "/foobar") + assert.Equal(t, httpReq.Method, "GET") + assert.Len(t, httpReq.Headers.Headers, 2) + assert.Equal(t, "value-1", httpReq.Headers.Get("header-1")) + assert.Equal(t, "value-2", httpReq.Headers.Get("header-2")) +} + +func TestHTTPToResp(t *testing.T) { + resp := &http.Response{ + Body: "this is body", + StatusCode: 123, + Headers: http.Headers{ + Headers: map[string]string{ + "h1": "v1", + "h2": "v2", + }, + }, + } + + apigwResp := HTTPToResp(resp) + assert.Equal(t, 123, apigwResp.StatusCode) + assert.Equal(t, "this is body", apigwResp.Body) + assert.Equal(t, "application/json", apigwResp.Headers["Content-Type"]) + assert.Len(t, apigwResp.Headers, 1) +} diff --git a/internal/bolt.go b/internal/bolt.go index 6ba6dc8..894c070 100644 --- a/internal/bolt.go +++ b/internal/bolt.go @@ -23,7 +23,7 @@ import ( "log" "os" - "github.com/boltdb/bolt" + bolt "go.etcd.io/bbolt" ) var boltdb *bolt.DB diff --git a/internal/handler/address.go b/internal/handler/address.go index b02dff7..ae79fe7 100644 --- a/internal/handler/address.go +++ b/internal/handler/address.go @@ -62,7 +62,7 @@ func GetAddressHash(hash hash.Hash, _ http.Request) *http.Response { return http.CreateError("hash not found", 404) } - if info == nil { + if info == nil || info.Deleted { log.Print(err) return http.CreateError("hash not found", 404) } @@ -153,7 +153,6 @@ func deleteAddressHashByOwner(addrHash hash.Hash, req http.Request) *http.Respon res, err := repo.Delete(current.Hash) if err != nil || !res { - log.Print(err) return http.CreateError("error while deleting record", 500) } @@ -201,13 +200,117 @@ func deleteAddressHashByOrganization(addrHash hash.Hash, organizationInfo *organ return http.CreateOutput("ok", 200) } +func SoftDeleteAddressHash(addrHash hash.Hash, req http.Request) *http.Response { + repo := address.GetResolveRepository() + current, err := repo.Get(addrHash.String()) + if err != nil { + return http.CreateError("error while fetching record", 500) + } + + if !req.ValidateAuthenticationToken(current.PubKey, current.Hash+current.RoutingID+strconv.FormatUint(current.Serial, 10)) { + return http.CreateError("unauthenticated", 401) + } + + if current == nil || current.Deleted { + return http.CreateError("cannot find record", 404) + } + + res, err := repo.SoftDelete(current.Hash) + if err != nil || !res { + return http.CreateError("error while deleting record", 500) + } + + return http.CreateOutput("", 204) +} + +func SoftUndeleteAddressHash(addrHash hash.Hash, req http.Request) *http.Response { + repo := address.GetResolveRepository() + current, err := repo.Get(addrHash.String()) + if err != nil { + return http.CreateError("error while fetching record", 500) + } + + if current == nil { + return http.CreateError("cannot find record", 404) + } + + if !req.ValidateAuthenticationToken(current.PubKey, current.Hash+current.RoutingID+strconv.FormatUint(current.Serial, 10)) { + return http.CreateError("unauthenticated", 401) + } + + res, err := repo.SoftUndelete(current.Hash) + if err != nil || !res { + return http.CreateError("error while undeleting record", 500) + } + + return http.CreateOutput("", 204) +} + +func GetKeyStatus(hash hash.Hash, req http.Request) *http.Response { + fp, ok := req.Params["fingerprint"] + if !ok { + return http.CreateError("not found", 404) + } + + repo := address.GetResolveRepository() + ks, err := repo.GetKeyStatus(hash.String(), fp) + if err != nil { + return http.CreateError("not found", 404) + } + + var statusMap = map[address.KeyStatus]int{ + address.KSNormal: 204, + address.KSCompromised: 410, + } + + status, ok := statusMap[ks] + if !ok { + status = 404 + } + + return http.CreateOutput("", status) +} + +func SetKeyStatus(hash hash.Hash, req http.Request) *http.Response { + fp, ok := req.Params["fingerprint"] + if !ok { + return http.CreateError("not found", 404) + } + + type setKeyRequestBody struct { + Status string `json:"status"` + } + + body := &setKeyRequestBody{} + if req.Body != "" { + err := json.Unmarshal([]byte(req.Body), body) + if err != nil { + log.Print(err) + return http.CreateError("invalid body data", 400) + } + } + + ks, err := address.StringToKeyStatus(body.Status) + if err != nil { + return http.CreateError("invalid status", 400) + } + + repo := address.GetResolveRepository() + err = repo.SetKeyStatus(hash.String(), fp, ks) + if err != nil { + return http.CreateError("error while updating", 400) + } + + return http.CreateOutput("key updated", 200) +} + func updateAddress(uploadBody addressUploadBody, req http.Request, current *address.ResolveInfoType) *http.Response { if !req.ValidateAuthenticationToken(current.PubKey, current.Hash+current.RoutingID+strconv.FormatUint(current.Serial, 10)) { return http.CreateError("unauthenticated", 401) } repo := address.GetResolveRepository() - res, err := repo.Update(current, uploadBody.RoutingID, uploadBody.PublicKey.String()) + res, err := repo.Update(current, uploadBody.RoutingID, uploadBody.PublicKey) if err != nil || !res { log.Print(err) @@ -229,7 +332,7 @@ func createAddress(addrHash hash.Hash, uploadBody addressUploadBody) *http.Respo } repo := address.GetResolveRepository() - res, err := repo.Create(addrHash.String(), uploadBody.RoutingID, uploadBody.PublicKey.String(), uploadBody.Proof.String()) + res, err := repo.Create(addrHash.String(), uploadBody.RoutingID, uploadBody.PublicKey, uploadBody.Proof.String()) if err != nil || !res { log.Print(err) return http.CreateError("error while creating: ", 500) diff --git a/internal/handler/address_test.go b/internal/handler/address_test.go index 2f9ef9c..cabfd65 100644 --- a/internal/handler/address_test.go +++ b/internal/handler/address_test.go @@ -56,13 +56,13 @@ func TestAddress(t *testing.T) { pow := proofofwork.New(22, addr.Hash().String(), 1540921) // Test fetching unknown hash - req := http.NewRequest("GET", "/", "") + req := http.NewRequest("GET", "/", "", nil) res := GetAddressHash("0CD8666848BF286D951C3D230E8B6E092FDE03C3A080E3454467E496E7B14E78", req) assert.Equal(t, 404, res.StatusCode) assert.JSONEq(t, `{ "error": "hash not found" }`, res.Body) // Insert illegal body - req = http.NewRequest("GET", "/", "illegal body that should error") + req = http.NewRequest("GET", "/", "illegal body that should error", nil) res = PostAddressHash("0CD8666848BF286D951C3D230E8B6E092FDE03C3A080E3454467E496E7B14E78", req) assert.Equal(t, 400, res.StatusCode) assert.JSONEq(t, `{ "error": "invalid data" }`, res.Body) @@ -95,7 +95,7 @@ func TestAddress(t *testing.T) { assert.Equal(t, `"created"`, res.Body) // Test fetching known hash - req = http.NewRequest("GET", "/", "") + req = http.NewRequest("GET", "/", "", nil) res = GetAddressHash(addr.Hash(), req) assert.Equal(t, 200, res.StatusCode) info := getAddressRecord(res) @@ -122,7 +122,7 @@ func TestValidateVerifyHashFailed(t *testing.T) { Proof: pow, }) - req := http.NewRequest("GET", "/", string(b)) + req := http.NewRequest("GET", "/", string(b), nil) res := PostAddressHash(addr.Hash(), req) assert.NotNil(t, res) assert.Equal(t, 400, res.StatusCode) @@ -146,7 +146,7 @@ func TestValidateVerifyNeedTokenForOrg(t *testing.T) { Proof: pow, }) - req := http.NewRequest("GET", "/", string(b)) + req := http.NewRequest("GET", "/", string(b), nil) res := PostAddressHash(addr.Hash(), req) assert.NotNil(t, res) assert.Equal(t, 400, res.StatusCode) @@ -170,7 +170,7 @@ func TestValidateVerifyNoTokenForNonOrg(t *testing.T) { Proof: pow, }) - req := http.NewRequest("GET", "/", string(b)) + req := http.NewRequest("GET", "/", string(b), nil) res := PostAddressHash(addr.Hash(), req) assert.NotNil(t, res) assert.Equal(t, 400, res.StatusCode) @@ -194,7 +194,7 @@ func TestValidateRoutingIDFailed(t *testing.T) { Proof: pow, }) - req := http.NewRequest("GET", "/", string(b)) + req := http.NewRequest("GET", "/", string(b), nil) res := PostAddressHash(addr.Hash(), req) assert.NotNil(t, res) assert.Equal(t, 400, res.StatusCode) @@ -217,7 +217,7 @@ func TestAddressUpdate(t *testing.T) { assert.NotNil(t, res) // Fetch addr1 record - req := http.NewRequest("GET", "/", "") + req := http.NewRequest("GET", "/", "", nil) res = GetAddressHash(addr1.Hash(), req) assert.Equal(t, 200, res.StatusCode) current := getAddressRecord(res) @@ -245,7 +245,7 @@ func TestAddressUpdate(t *testing.T) { authToken := http.GenerateAuthenticationToken([]byte(sig), *privKey) // Update record with correct auth - req = http.NewRequest("GET", "/", "") + req = http.NewRequest("GET", "/", "", nil) req.Headers.Set("authorization", "BEARER "+authToken) // req.Headers.Set("authorization", "BEARER 2UxSWVAUJ/iIr59x76B9bF/CeQXDi4dTY4D73P8iJwE/CRaIpRyg1RHMbfLVM6fz3sfOammn8wzhooxfv6BVAg==") @@ -254,7 +254,7 @@ func TestAddressUpdate(t *testing.T) { assert.Equal(t, 200, res.StatusCode) assert.Equal(t, `"updated"`, res.Body) - req = http.NewRequest("GET", "/", "") + req = http.NewRequest("GET", "/", "", nil) res = GetAddressHash(addr1.Hash(), req) assert.Equal(t, 200, res.StatusCode) info := getAddressRecord(res) @@ -280,29 +280,29 @@ func TestAddressDeletion(t *testing.T) { assert.NotNil(t, res) // Delete hash without auth - req := http.NewRequest("GET", "/", "") + req := http.NewRequest("GET", "/", "", nil) req.Headers.Set("authorization", "Bearer sdfafsadf") - res = DeleteAddressHash("efd5631354d823cd64aa8df8149cc317ae30d319295b491e86e9a5ffdab8fd7e", req) + res = DeleteAddressHash(addr1.Hash(), req) assert.Equal(t, 401, res.StatusCode) - req = http.NewRequest("GET", "/", "") - res = GetAddressHash("efd5631354d823cd64aa8df8149cc317ae30d319295b491e86e9a5ffdab8fd7e", req) + req = http.NewRequest("GET", "/", "", nil) + res = GetAddressHash(addr1.Hash(), req) assert.Equal(t, 200, res.StatusCode) // Delete hash with wrong auth - req = http.NewRequest("GET", "/", "") + req = http.NewRequest("GET", "/", "", nil) req.Headers.Set("authorization", "BEARER okqF4rW/bFoNvmxk29NLb3lbTHCpir8A86i4IiK0j6211+WMOFCr91RodeBLSCXx167VOhC/++") - res = DeleteAddressHash("efd5631354d823cd64aa8df8149cc317ae30d319295b491e86e9a5ffdab8fd7e", req) + res = DeleteAddressHash(addr1.Hash(), req) assert.Equal(t, 401, res.StatusCode) // Delete wrong hash with wrong auth - req = http.NewRequest("GET", "/", "") + req = http.NewRequest("GET", "/", "", nil) req.Headers.Set("authorization", "BEARER okqF4rW/bFoNvmxk29NLb3lbTHCpir8A86i4IiK0j6211+WMOFCr91RodeBLSCXx167VOhC/++wes1RLx7Q1O26cmcvpsAV/7I0e+ISDSzHHW82zuvLw0IaqZ7xngrkz4QdG00VGi3mS6bNSjQqU4Yxrqoiwk/o/jVD0/MHLxYbJHn+taL2sEeSMBvfkc5zHoqsNAgZQ7anvAsYASF30NR3pGvp/66P801sYxJYrIv4b48U2Z3pQZHozDY2e4YUA+14ZWZIYqQ+K8yCa78KTSTy5mDznP2Hpvnsy6sT8R93u2aLk++vLCmRby3REGfYRaWDxSGxgXjCgVqiLdFRLhg==") res = DeleteAddressHash("00000000000000000000000000000317ae30d319295b491e86e9a5ffdab8fd7e", req) assert.Equal(t, 500, res.StatusCode) // Fetch addr1 record - req = http.NewRequest("GET", "/", "") + req = http.NewRequest("GET", "/", "", nil) res = GetAddressHash(addr1.Hash(), req) assert.Equal(t, 200, res.StatusCode) current := getAddressRecord(res) @@ -313,16 +313,89 @@ func TestAddressDeletion(t *testing.T) { authToken := http.GenerateAuthenticationToken([]byte(sig), *privKey) // Delete hash with auth - req = http.NewRequest("GET", "/", "") + req = http.NewRequest("GET", "/", "", nil) req.Headers.Set("authorization", "BEARER "+authToken) - res = DeleteAddressHash("efd5631354d823cd64aa8df8149cc317ae30d319295b491e86e9a5ffdab8fd7e", req) + res = DeleteAddressHash(addr1.Hash(), req) + assert.Equal(t, 200, res.StatusCode) + + req = http.NewRequest("GET", "/", "", nil) + res = GetAddressHash(addr1.Hash(), req) + assert.Equal(t, 404, res.StatusCode) + res = GetAddressHash(addr2.Hash(), req) + assert.Equal(t, 200, res.StatusCode) +} + +func TestAddressSoftDeletion(t *testing.T) { + setupRepo() + + addr1, _ := pkgAddress.NewAddress("foo!") + pow1 := proofofwork.New(22, addr1.Hash().String(), 1310761) + + addr2, _ := pkgAddress.NewAddress("bar!") + pow2 := proofofwork.New(22, addr2.Hash().String(), 1019732) + + // Insert some records + res := insertAddressRecord(*addr1, "../../testdata/key-3.json", fakeRoutingId.String(), "", pow1) + assert.NotNil(t, res) + res = insertAddressRecord(*addr2, "../../testdata/key-4.json", fakeRoutingId.String(), "", pow2) + assert.NotNil(t, res) + + // Delete hash without auth + req := http.NewRequest("POST", "/", "", nil) + req.Headers.Set("authorization", "Bearer sdfafsadf") + res = SoftDeleteAddressHash(addr1.Hash(), req) + assert.Equal(t, 401, res.StatusCode) + + req = http.NewRequest("GET", "/", "", nil) + res = GetAddressHash(addr1.Hash(), req) + assert.Equal(t, 200, res.StatusCode) + + // Delete hash with wrong auth + req = http.NewRequest("GET", "/", "", nil) + req.Headers.Set("authorization", "BEARER okqF4rW/bFoNvmxk29NLb3lbTHCpir8A86i4IiK0j6211+WMOFCr91RodeBLSCXx167VOhC/++") + res = SoftDeleteAddressHash(addr1.Hash(), req) + assert.Equal(t, 401, res.StatusCode) + + // Delete wrong hash with wrong auth + req = http.NewRequest("GET", "/", "", nil) + req.Headers.Set("authorization", "BEARER okqF4rW/bFoNvmxk29NLb3lbTHCpir8A86i4IiK0j6211+WMOFCr91RodeBLSCXx167VOhC/++wes1RLx7Q1O26cmcvpsAV/7I0e+ISDSzHHW82zuvLw0IaqZ7xngrkz4QdG00VGi3mS6bNSjQqU4Yxrqoiwk/o/jVD0/MHLxYbJHn+taL2sEeSMBvfkc5zHoqsNAgZQ7anvAsYASF30NR3pGvp/66P801sYxJYrIv4b48U2Z3pQZHozDY2e4YUA+14ZWZIYqQ+K8yCa78KTSTy5mDznP2Hpvnsy6sT8R93u2aLk++vLCmRby3REGfYRaWDxSGxgXjCgVqiLdFRLhg==") + res = SoftDeleteAddressHash("00000000000000000000000000000317ae30d319295b491e86e9a5ffdab8fd7e", req) + assert.Equal(t, 500, res.StatusCode) + + // Fetch addr1 record + req = http.NewRequest("GET", "/", "", nil) + res = GetAddressHash(addr1.Hash(), req) assert.Equal(t, 200, res.StatusCode) + current := getAddressRecord(res) + + // Create authentication token + privKey, _, _ := testing2.ReadTestKey("../../testdata/key-3.json") + sig := current.Hash + current.RoutingID + strconv.FormatUint(current.Serial, 10) + authToken := http.GenerateAuthenticationToken([]byte(sig), *privKey) + + // Soft delete hash with auth + req = http.NewRequest("GET", "/", "", nil) + req.Headers.Set("authorization", "BEARER "+authToken) + res = SoftDeleteAddressHash(addr1.Hash(), req) + assert.Equal(t, 204, res.StatusCode) - req = http.NewRequest("GET", "/", "") + req = http.NewRequest("GET", "/", "", nil) res = GetAddressHash(addr1.Hash(), req) assert.Equal(t, 404, res.StatusCode) res = GetAddressHash(addr2.Hash(), req) assert.Equal(t, 200, res.StatusCode) + + // Soft undelete hash with auth + req = http.NewRequest("GET", "/", "", nil) + req.Headers.Set("authorization", "BEARER "+authToken) + res = SoftUndeleteAddressHash(addr1.Hash(), req) + assert.Equal(t, 204, res.StatusCode) + + req = http.NewRequest("GET", "/", "", nil) + res = GetAddressHash(addr1.Hash(), req) + assert.Equal(t, 200, res.StatusCode) + res = GetAddressHash(addr2.Hash(), req) + assert.Equal(t, 200, res.StatusCode) } func TestAddOrganisationalAddresses(t *testing.T) { @@ -379,7 +452,7 @@ func TestAddOrganisationalAddresses(t *testing.T) { assert.Contains(t, res.Body, "created") // Check if record exists - req := http.NewRequest("GET", "/", "") + req := http.NewRequest("GET", "/", "", nil) res = GetAddressHash(addr.Hash(), req) assert.Equal(t, 200, res.StatusCode) info := getAddressRecord(res) @@ -424,7 +497,7 @@ func TestAllowUpdateToOrgAddressWithoutToken(t *testing.T) { assert.NoError(t, err) // Get serial - req := http.NewRequest("GET", "/", "") + req := http.NewRequest("GET", "/", "", nil) res = GetAddressHash(addr.Hash(), req) assert.Equal(t, 200, res.StatusCode) info := getAddressRecord(res) @@ -432,7 +505,7 @@ func TestAllowUpdateToOrgAddressWithoutToken(t *testing.T) { sig := addr.Hash().String() + info.RoutingID + strconv.FormatUint(info.Serial, 10) authToken := http.GenerateAuthenticationToken([]byte(sig), *privKey) - req = http.NewRequest("POST", "/account/"+addr.Hash().String(), string(b)) + req = http.NewRequest("POST", "/account/"+addr.Hash().String(), string(b), nil) req.Headers.Set("authorization", "BEARER "+authToken) res = PostAddressHash(addr.Hash(), req) assert.Equal(t, "\"updated\"", res.Body) @@ -445,15 +518,15 @@ func TestDeleteOrganisationalAddresses(t *testing.T) { // Add organisation orgHash1 := hash.New("acme-inc") pow1 := proofofwork.New(22, orgHash1.String(), 1305874) - res := insertOrganisationRecord(orgHash1, "../../testdata/key-5.json", pow1, []string{}) + _ = insertOrganisationRecord(orgHash1, "../../testdata/key-5.json", pow1, []string{}) orgHash2 := hash.New("example") pow2 := proofofwork.New(22, orgHash2.String(), 190734) - res = insertOrganisationRecord(orgHash2, "../../testdata/key-6.json", pow2, []string{}) + _ = insertOrganisationRecord(orgHash2, "../../testdata/key-6.json", pow2, []string{}) orgHash3 := hash.New("another") pow3 := proofofwork.New(22, orgHash3.String(), 21232) - res = insertOrganisationRecord(orgHash3, "../../testdata/key-7.json", pow3, []string{}) + _ = insertOrganisationRecord(orgHash3, "../../testdata/key-7.json", pow3, []string{}) addr, _ := pkgAddress.NewAddress("example@acme-inc!") pow4 := proofofwork.New(22, addr.Hash().String(), 11741366) @@ -462,13 +535,13 @@ func TestDeleteOrganisationalAddresses(t *testing.T) { privKey, _, _ := testing2.ReadTestKey("../../testdata/key-5.json") inviteToken := address.GenerateToken(addr.Hash(), fakeRoutingId.String(), time.Date(2010, 05, 05, 12, 0, 0, 0, time.UTC), *privKey) - res = insertAddressRecord(*addr, "../../testdata/key-4.json", fakeRoutingId.String(), inviteToken, pow4) + res := insertAddressRecord(*addr, "../../testdata/key-4.json", fakeRoutingId.String(), inviteToken, pow4) assert.NotNil(t, res) assert.Equal(t, 201, res.StatusCode) assert.Contains(t, res.Body, "created") // Check if record exists - req := http.NewRequest("GET", "/", "") + req := http.NewRequest("GET", "/", "", nil) res = GetAddressHash(addr.Hash(), req) assert.Equal(t, 200, res.StatusCode) } @@ -478,8 +551,8 @@ func TestDeleteOrganisationalAddresses(t *testing.T) { insertRecords() // Fetch addr record - req := http.NewRequest("GET", "/", "") - res = GetAddressHash(addr.Hash(), req) + req := http.NewRequest("GET", "/", "", nil) + res := GetAddressHash(addr.Hash(), req) assert.Equal(t, 200, res.StatusCode) current := getAddressRecord(res) @@ -489,13 +562,13 @@ func TestDeleteOrganisationalAddresses(t *testing.T) { authToken := http.GenerateAuthenticationToken([]byte(sig), *privKey) // Delete as "regular" user - req = http.NewRequest("GET", "/", "") + req = http.NewRequest("GET", "/", "", nil) // req.Headers.Set("authorization", "BEARER 2UxSWVAUJ/iIr59x76B9bF/CeQXDi4dTY4D73P8iJwE/CRaIpRyg1RHMbfLVM6fz3sfOammn8wzhooxfv6BVAg==") req.Headers.Set("authorization", "BEARER "+authToken) res = DeleteAddressHash(addr.Hash(), req) assert.Equal(t, 200, res.StatusCode) - req = http.NewRequest("GET", "/", "") + req = http.NewRequest("GET", "/", "", nil) res = GetAddressHash(addr.Hash(), req) assert.Equal(t, 404, res.StatusCode) @@ -509,12 +582,12 @@ func TestDeleteOrganisationalAddresses(t *testing.T) { authToken = http.GenerateAuthenticationToken([]byte(sig), *privKey) // Delete as correct "organisation" user, but without body - req = http.NewRequest("GET", "/", "") + req = http.NewRequest("GET", "/", "", nil) req.Headers.Set("authorization", "BEARER "+authToken) res = DeleteAddressHash(addr.Hash(), req) assert.Equal(t, 401, res.StatusCode) - req = http.NewRequest("GET", "/", "") + req = http.NewRequest("GET", "/", "", nil) res = GetAddressHash(addr.Hash(), req) assert.Equal(t, 200, res.StatusCode) @@ -526,13 +599,13 @@ func TestDeleteOrganisationalAddresses(t *testing.T) { } b, _ := json.Marshal(ob) - req = http.NewRequest("GET", "/", string(b)) + req = http.NewRequest("GET", "/", string(b), nil) req.Headers.Set("authorization", "BEARER "+authToken) res = DeleteAddressHash(addr.Hash(), req) assert.Equal(t, 401, res.StatusCode) assert.Contains(t, res.Body, "error validating address") - req = http.NewRequest("GET", "/", "") + req = http.NewRequest("GET", "/", "", nil) res = GetAddressHash(addr.Hash(), req) assert.Equal(t, 200, res.StatusCode) @@ -544,12 +617,12 @@ func TestDeleteOrganisationalAddresses(t *testing.T) { } b, _ = json.Marshal(ob) - req = http.NewRequest("GET", "/", string(b)) + req = http.NewRequest("GET", "/", string(b), nil) req.Headers.Set("authorization", "BEARER "+authToken) res = DeleteAddressHash(addr.Hash(), req) assert.Equal(t, 200, res.StatusCode) - req = http.NewRequest("GET", "/", "") + req = http.NewRequest("GET", "/", "", nil) res = GetAddressHash(addr.Hash(), req) assert.Equal(t, 404, res.StatusCode) @@ -563,16 +636,82 @@ func TestDeleteOrganisationalAddresses(t *testing.T) { // Delete as incorrect "other organisation" user insertRecords() - req = http.NewRequest("GET", "/", "") + req = http.NewRequest("GET", "/", "", nil) req.Headers.Set("authorization", "BEARER "+authToken) res = DeleteAddressHash(addr.Hash(), req) assert.Equal(t, 401, res.StatusCode) - req = http.NewRequest("GET", "/", "") + req = http.NewRequest("GET", "/", "", nil) res = GetAddressHash(addr.Hash(), req) assert.Equal(t, 200, res.StatusCode) } +func TestHistory(t *testing.T) { + setupRepo() + + addr, _ := pkgAddress.NewAddress("example!") + addr2, _ := pkgAddress.NewAddress("someoneelse!") + pow := proofofwork.New(22, addr.Hash().String(), 1540921) + + _, pub, _ := testing2.ReadTestKey("../../testdata/key-4.json") + _, pub2, _ := testing2.ReadTestKey("../../testdata/key-3.json") + + // Insert new hash + res := insertAddressRecord(*addr, "../../testdata/key-4.json", fakeRoutingId.String(), "", pow) + assert.NotNil(t, res) + assert.Equal(t, 201, res.StatusCode) + assert.Equal(t, `"created"`, res.Body) + + // Test history of key + req := http.NewRequest("GET", "/address/"+addr.Hash().String()+"/check/"+pub.Fingerprint(), "", map[string]string{ + "fingerprint": pub.Fingerprint(), + }) + res = GetKeyStatus(addr.Hash(), req) + assert.Equal(t, 204, res.StatusCode) + + // Check history of non-existing key + req = http.NewRequest("GET", "/address/"+addr.Hash().String()+"/check/"+pub2.Fingerprint(), "", map[string]string{ + "fingerprint": pub2.Fingerprint(), + }) + res = GetKeyStatus(addr.Hash(), req) + assert.Equal(t, 404, res.StatusCode) + + // Check history of existing key on different account + req = http.NewRequest("GET", "/address/"+addr2.Hash().String()+"/check/"+pub.Fingerprint(), "", map[string]string{ + "fingerprint": pub.Fingerprint(), + }) + res = GetKeyStatus(addr2.Hash(), req) + assert.Equal(t, 404, res.StatusCode) + + // Set key to unknown status + req = http.NewRequest("GET", "/address/"+addr.Hash().String()+"/check/"+pub.Fingerprint(), "{\"status\":\"unknown\"}", map[string]string{ + "fingerprint": pub.Fingerprint(), + }) + res = SetKeyStatus(addr.Hash(), req) + assert.Equal(t, 400, res.StatusCode) + + // Set key to compromised + req = http.NewRequest("GET", "/address/"+addr.Hash().String()+"/check/"+pub.Fingerprint(), "{\"status\":\"compromised\"}", map[string]string{ + "fingerprint": pub.Fingerprint(), + }) + res = SetKeyStatus(addr.Hash(), req) + assert.Equal(t, 200, res.StatusCode) + + // Check history of key again + req = http.NewRequest("GET", "/address/"+addr.Hash().String()+"/check/"+pub.Fingerprint(), "", map[string]string{ + "fingerprint": pub.Fingerprint(), + }) + res = GetKeyStatus(addr.Hash(), req) + assert.Equal(t, 410, res.StatusCode) + + // Set key to normal + req = http.NewRequest("GET", "/address/"+addr.Hash().String()+"/check/"+pub.Fingerprint(), "{\"status\":\"normal\"}", map[string]string{ + "fingerprint": pub.Fingerprint(), + }) + res = SetKeyStatus(addr.Hash(), req) + assert.Equal(t, 200, res.StatusCode) +} + func setupRepo() { sr := address.NewSqliteResolver(":memory:") address.SetDefaultRepository(sr) @@ -622,7 +761,7 @@ func insertAddressRecord(addr pkgAddress.Address, keyPath, routingId, orgToken s if err != nil { return nil } - req := http.NewRequest("GET", "/", string(b)) + req := http.NewRequest("GET", "/", string(b), nil) return PostAddressHash(addr.Hash(), req) } diff --git a/internal/handler/organisation.go b/internal/handler/organisation.go index 4138107..6a5201f 100644 --- a/internal/handler/organisation.go +++ b/internal/handler/organisation.go @@ -159,3 +159,11 @@ func validateOrganisationBody(_ organisationUploadBody) bool { // PubKey and proof are already validated through the JSON marshalling return true } + +func SoftDeleteOrganisationHash(orgHash hash.Hash, req http.Request) *http.Response { + return nil +} + +func SoftUndeleteOrganisationHash(orgHash hash.Hash, req http.Request) *http.Response { + return nil +} diff --git a/internal/handler/organisation_test.go b/internal/handler/organisation_test.go index e37482d..a0f3723 100644 --- a/internal/handler/organisation_test.go +++ b/internal/handler/organisation_test.go @@ -52,13 +52,13 @@ func TestOrganisation(t *testing.T) { pow := proofofwork.New(22, orgHash.String(), 1783097) // Test fetching unknown hash - req := http.NewRequest("GET", "/", "") + req := http.NewRequest("GET", "/", "", nil) res := GetOrganisationHash(orgHash, req) assert.Equal(t, 404, res.StatusCode) assert.Contains(t, res.Body, "hash not found") // Insert illegal body - req = http.NewRequest("GET", "/", "illegal body that should error") + req = http.NewRequest("GET", "/", "illegal body that should error", nil) res = PostOrganisationHash(orgHash, req) assert.Equal(t, 400, res.StatusCode) assert.Contains(t, res.Body, "invalid data") @@ -70,7 +70,7 @@ func TestOrganisation(t *testing.T) { assert.Equal(t, `"created"`, res.Body) // Test fetching known hash - req = http.NewRequest("GET", "/", "") + req = http.NewRequest("GET", "/", "", nil) res = GetOrganisationHash(orgHash, req) assert.Equal(t, 200, res.StatusCode) info := getOrganisationRecord(res) @@ -99,7 +99,7 @@ func TestOrganisationUpdate(t *testing.T) { assert.NotNil(t, res) // Fetch record - req := http.NewRequest("GET", "/", "") + req := http.NewRequest("GET", "/", "", nil) res = GetOrganisationHash(orgHash1, req) assert.Equal(t, 200, res.StatusCode) current := getOrganisationRecord(res) @@ -121,14 +121,14 @@ func TestOrganisationUpdate(t *testing.T) { authToken := http.GenerateAuthenticationToken([]byte(sig), *privKey) // Update record with correct auth - req = http.NewRequest("GET", "/", "") + req = http.NewRequest("GET", "/", "", nil) req.Headers.Set("authorization", "BEARER "+authToken) setRepoTime(time.Date(2010, 12, 13, 12, 34, 56, 1241511, time.UTC)) res = updateOrganisation(*body, req, ¤t) assert.Equal(t, 200, res.StatusCode) assert.Equal(t, `"updated"`, res.Body) - req = http.NewRequest("GET", "/", "") + req = http.NewRequest("GET", "/", "", nil) res = GetOrganisationHash(orgHash1, req) assert.Equal(t, 200, res.StatusCode) info := getOrganisationRecord(res) @@ -155,27 +155,27 @@ func TestOrganisationDeletion(t *testing.T) { assert.NotNil(t, res) // Delete hash without auth - req := http.NewRequest("GET", "/", "") + req := http.NewRequest("GET", "/", "", nil) req.Headers.Set("authorization", "Bearer sdfafsadf") res = DeleteOrganisationHash(orgHash1, req) assert.Equal(t, 401, res.StatusCode) - req = http.NewRequest("GET", "/", "") + req = http.NewRequest("GET", "/", "", nil) res = GetOrganisationHash(orgHash1, req) assert.Equal(t, 200, res.StatusCode) - req = http.NewRequest("GET", "/", "") + req = http.NewRequest("GET", "/", "", nil) res = GetOrganisationHash(orgHash2, req) assert.Equal(t, 200, res.StatusCode) // Delete hash with wrong auth - req = http.NewRequest("GET", "/", "") + req = http.NewRequest("GET", "/", "", nil) req.Headers.Set("authorization", "BEARER okqF4rW/bFoNvmxk29NLb3lbTHCpir8A86i4IiK0j6211+WMOFCr91RodeBLSCXx167VOhC/++") res = DeleteOrganisationHash(orgHash1, req) assert.Equal(t, 401, res.StatusCode) // Fetch record - req = http.NewRequest("GET", "/", "") + req = http.NewRequest("GET", "/", "", nil) res = GetOrganisationHash(orgHash1, req) assert.Equal(t, 200, res.StatusCode) current := getOrganisationRecord(res) @@ -186,23 +186,23 @@ func TestOrganisationDeletion(t *testing.T) { authToken := http.GenerateAuthenticationToken([]byte(sig), *privKey) // Delete wrong hash with correct auth - req = http.NewRequest("GET", "/", "") + req = http.NewRequest("GET", "/", "", nil) req.Headers.Set("authorization", "BEARER "+authToken) res = DeleteOrganisationHash("0000000000000000000000000E8B6E092FDE03C3A080E3454467E496E7B14E78", req) assert.Equal(t, 500, res.StatusCode) // Delete hash with auth - req = http.NewRequest("GET", "/", "") + req = http.NewRequest("GET", "/", "", nil) // req.Headers.Set("authorization", "BEARER neftRnbcaw2mfudfSkXgBT6SJ3nEXsWzyumiIcDed8y6pBoEPkJkgqCHcwqm9TuqVycjzb3PemDYfvMmUfL9BA==") req.Headers.Set("authorization", "BEARER "+authToken) res = DeleteOrganisationHash(orgHash1, req) assert.Equal(t, 200, res.StatusCode) - req = http.NewRequest("GET", "/", "") + req = http.NewRequest("GET", "/", "", nil) res = GetOrganisationHash(orgHash1, req) assert.Equal(t, 404, res.StatusCode) - req = http.NewRequest("GET", "/", "") + req = http.NewRequest("GET", "/", "", nil) res = GetOrganisationHash(orgHash2, req) assert.Equal(t, 200, res.StatusCode) } @@ -221,7 +221,7 @@ func insertOrganisationRecord(orgHash hash.Hash, keyPath string, pow *proofofwor if err != nil { return nil } - req := http.NewRequest("GET", "/", string(b)) + req := http.NewRequest("GET", "/", string(b), nil) return PostOrganisationHash(orgHash, req) } diff --git a/internal/handler/routing_test.go b/internal/handler/routing_test.go index c1cde3c..2e21718 100644 --- a/internal/handler/routing_test.go +++ b/internal/handler/routing_test.go @@ -49,13 +49,13 @@ func TestRouting(t *testing.T) { sr.TimeNow = time.Date(2010, 04, 07, 12, 34, 56, 0, time.UTC) // Test fetching unknown hash - req := http.NewRequest("GET", "/", "") + req := http.NewRequest("GET", "/", "", nil) res := GetRoutingHash("0CD8666848BF286D951C3D230E8B6E092FDE03C3A080E3454467E496E7B14E78", req) assert.Equal(t, 404, res.StatusCode) assert.JSONEq(t, `{ "error": "hash not found" }`, res.Body) // Insert illegal body - req = http.NewRequest("GET", "/", "illegal body that should error") + req = http.NewRequest("GET", "/", "illegal body that should error", nil) res = PostRoutingHash("0CD8666848BF286D951C3D230E8B6E092FDE03C3A080E3454467E496E7B14E78", req) assert.Equal(t, 400, res.StatusCode) assert.JSONEq(t, `{ "error": "invalid data" }`, res.Body) @@ -67,7 +67,7 @@ func TestRouting(t *testing.T) { assert.Equal(t, `"created"`, res.Body) // Test fetching known hash - req = http.NewRequest("GET", "/", "") + req = http.NewRequest("GET", "/", "", nil) res = GetRoutingHash("0CD8666848BF286D951C3D230E8B6E092FDE03C3A080E3454467E496E7B14E78", req) assert.Equal(t, 200, res.StatusCode) info := getRoutingRecord(res) @@ -89,7 +89,7 @@ func TestRoutingUpdate(t *testing.T) { assert.NotNil(t, res) // Fetch record - req := http.NewRequest("GET", "/", "") + req := http.NewRequest("GET", "/", "", nil) res = GetRoutingHash("0CD8666848BF286D951C3D230E8B6E092FDE03C3A080E3454467E496E7B14E78", req) assert.Equal(t, 200, res.StatusCode) current := getRoutingRecord(res) @@ -111,7 +111,7 @@ func TestRoutingUpdate(t *testing.T) { authToken := http.GenerateAuthenticationToken([]byte(sig), *privKey) // Update record with correct auth - req = http.NewRequest("GET", "/", "") + req = http.NewRequest("GET", "/", "", nil) // req.Headers.Set("authorization", "BEARER okqF4rW/bFoNvmxk29NLb3lbTHCpir8A86i4IiK0j6211+WMOFCr91RodeBLSCXx167VOhC/++wes1RLx7Q1O26cmcvpsAV/7I0e+ISDSzHHW82zuvLw0IaqZ7xngrkz4QdG00VGi3mS6bNSjQqU4Yxrqoiwk/o/jVD0/MHLxYbJHn+taL2sEeSMBvfkc5zHoqsNAgZQ7anvAsYASF30NR3pGvp/66P801sYxJYrIv4b48U2Z3pQZHozDY2e4YUA+14ZWZIYqQ+K8yCa78KTSTy5mDznP2Hpvnsy6sT8R93u2aLk++vLCmRby3REGfYRaWDxSGxgXjCgVqiLdFRLhg==") req.Headers.Set("authorization", "BEARER "+authToken) sr.TimeNow = time.Date(2010, 12, 13, 12, 34, 56, 1241511, time.UTC) @@ -119,7 +119,7 @@ func TestRoutingUpdate(t *testing.T) { assert.Equal(t, 200, res.StatusCode) assert.Equal(t, `"updated"`, res.Body) - req = http.NewRequest("GET", "/", "") + req = http.NewRequest("GET", "/", "", nil) res = GetRoutingHash("0CD8666848BF286D951C3D230E8B6E092FDE03C3A080E3454467E496E7B14E78", req) assert.Equal(t, 200, res.StatusCode) info := getRoutingRecord(res) @@ -141,34 +141,34 @@ func TestRoutingDeletion(t *testing.T) { assert.NotNil(t, res) // Delete hash without auth - req := http.NewRequest("GET", "/", "") + req := http.NewRequest("GET", "/", "", nil) req.Headers.Set("authorization", "Bearer sdfafsadf") res = DeleteRoutingHash("0CD8666848BF286D951C3D230E8B6E092FDE03C3A080E3454467E496E7B14E78", req) assert.Equal(t, 401, res.StatusCode) - req = http.NewRequest("GET", "/", "") + req = http.NewRequest("GET", "/", "", nil) res = GetRoutingHash("0CD8666848BF286D951C3D230E8B6E092FDE03C3A080E3454467E496E7B14E78", req) assert.Equal(t, 200, res.StatusCode) // Delete hash with wrong auth - req = http.NewRequest("GET", "/", "") + req = http.NewRequest("GET", "/", "", nil) req.Headers.Set("authorization", "BEARER okqF4rW/bFoNvmxk29NLb3lbTHCpir8A86i4IiK0j6211+WMOFCr91RodeBLSCXx167VOhC/++") res = DeleteRoutingHash("0CD8666848BF286D951C3D230E8B6E092FDE03C3A080E3454467E496E7B14E78", req) assert.Equal(t, 401, res.StatusCode) // Delete wrong hash with wrong auth - req = http.NewRequest("GET", "/", "") + req = http.NewRequest("GET", "/", "", nil) req.Headers.Set("authorization", "BEARER okqF4rW/bFoNvmxk29NLb3lbTHCpir8A86i4IiK0j6211+WMOFCr91RodeBLSCXx167VOhC/++wes1RLx7Q1O26cmcvpsAV/7I0e+ISDSzHHW82zuvLw0IaqZ7xngrkz4QdG00VGi3mS6bNSjQqU4Yxrqoiwk/o/jVD0/MHLxYbJHn+taL2sEeSMBvfkc5zHoqsNAgZQ7anvAsYASF30NR3pGvp/66P801sYxJYrIv4b48U2Z3pQZHozDY2e4YUA+14ZWZIYqQ+K8yCa78KTSTy5mDznP2Hpvnsy6sT8R93u2aLk++vLCmRby3REGfYRaWDxSGxgXjCgVqiLdFRLhg==") res = DeleteRoutingHash("0000000000000000000000000E8B6E092FDE03C3A080E3454467E496E7B14E78", req) assert.Equal(t, 500, res.StatusCode) // Delete hash with auth - req = http.NewRequest("GET", "/", "") + req = http.NewRequest("GET", "/", "", nil) req.Headers.Set("authorization", "BEARER okqF4rW/bFoNvmxk29NLb3lbTHCpir8A86i4IiK0j6211+WMOFCr91RodeBLSCXx167VOhC/++wes1RLx7Q1O26cmcvpsAV/7I0e+ISDSzHHW82zuvLw0IaqZ7xngrkz4QdG00VGi3mS6bNSjQqU4Yxrqoiwk/o/jVD0/MHLxYbJHn+taL2sEeSMBvfkc5zHoqsNAgZQ7anvAsYASF30NR3pGvp/66P801sYxJYrIv4b48U2Z3pQZHozDY2e4YUA+14ZWZIYqQ+K8yCa78KTSTy5mDznP2Hpvnsy6sT8R93u2aLk++vLCmRby3REGfYRaWDxSGxgXjCgVqiLdFRLhg==") res = DeleteRoutingHash("0CD8666848BF286D951C3D230E8B6E092FDE03C3A080E3454467E496E7B14E78", req) assert.Equal(t, 200, res.StatusCode) - req = http.NewRequest("GET", "/", "") + req = http.NewRequest("GET", "/", "", nil) res = GetRoutingHash("0CD8666848BF286D951C3D230E8B6E092FDE03C3A080E3454467E496E7B14E78", req) assert.Equal(t, 404, res.StatusCode) res = GetRoutingHash("c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2", req) @@ -188,7 +188,7 @@ func insertRoutingRecord(routingHash hash.Hash, keyPath string, routing string) if err != nil { return nil } - req := http.NewRequest("GET", "/", string(b)) + req := http.NewRequest("GET", "/", string(b), nil) return PostRoutingHash(routingHash, req) } diff --git a/internal/http/http.go b/internal/http/http.go index e59877d..c52a6bd 100644 --- a/internal/http/http.go +++ b/internal/http/http.go @@ -29,6 +29,7 @@ import ( "strings" "github.com/bitmaelum/bitmaelum-suite/pkg/bmcrypto" + "github.com/gorilla/mux" ) type Headers struct { @@ -60,14 +61,16 @@ type Request struct { URL string Body string Headers Headers + Params map[string]string } -func NewRequest(method, url, body string) Request { +func NewRequest(method, url, body string, params map[string]string) Request { return Request{ Method: method, URL: url, Body: body, Headers: NewHeaders(), + Params: params, } } @@ -148,10 +151,11 @@ func (r Request) ValidateAuthenticationToken(pubKey, hashData string) bool { func NetReqToReq(r http.Request) Request { b, err := ioutil.ReadAll(r.Body) if err != nil { - return NewRequest("", "", "") + return NewRequest("", "", "", nil) } - req := NewRequest(r.Method, r.URL.String(), string(b)) + params := mux.Vars(&r) + req := NewRequest(r.Method, r.URL.String(), string(b), params) // Add headers for k, v := range r.Header { diff --git a/internal/http/http_test.go b/internal/http/http_test.go index 22a5015..b949476 100644 --- a/internal/http/http_test.go +++ b/internal/http/http_test.go @@ -25,11 +25,11 @@ import ( "os" "testing" + testing2 "github.com/bitmaelum/key-resolver-go/internal/testing" "github.com/stretchr/testify/assert" ) const ( - // PrivKeyData string = "rsa MIICXQIBAAKBgQC57qC/BeoYcM6ijazuaCdJkbT8pvPpFEDVzf9ZQ9axswXU3mywSOaR3wflriSjmvRfUNs/BAjshgtJqgviUXx7lE5aG9mcUyvomyFFpfCR2l2Lvow0H8y7JoL6yxMSQf8gpAcaQzPB8dsfGe+DqA+5wjxXPOhC1QUcllt08yBB3wIDAQABAoGBAKSWDKsrtB5wdSnFmcfsYKKqHXjs3Mp9CCt6z0eYWoswesAFKFcgISINOLNi5MICX8GkFIACtVeSDJnnsd9j3HkRD7kwxmvVVXltaIrbrEunKgdRK1ACk2Bkb7UUDImDjiZztJvCSL+WLu9Fphn8IfPzwAIPWAKKBoD1kuI6yfFBAkEA6dJpoTMKDlCEMeJWZVUnhL7K/OBphWLO7cZfaxowBeGGXuMBWiaySsdeIDV7S/PDnoHBKwIkSsSfjzWYptuq4QJBAMuRXwoqZHKPHjMTCyz3C7nwFCzgmmKM5PReZU0s4/tdFu/VGOSnVDSzC5JFcY48Cs03TBwZ2wPhv/3r4a7YRL8CQQCxedRTVro7Q0IT2whYwdnNGERazLtLU0RdlkS2tpnc3OFxBDzygIyz1b/MEswTSmMg3LwSOP3zAmtZ+AR2IiYBAkBENgnqlhniaSJtaswr3PwI6fFYuEoDC8MMPzUijxA1ghPVeUpGE+ubXQNbl/lc97GG4iiWofNJcbOrmgadV8pxAkBhsn2T9DSoDdeImRecxs3/Xej4EYQpj/3X436RrFntjT06wD6wF9s5CvPmz/ftUBJ71IVlBQUd3jOgQPRzEhNC" PubKeyData string = "rsa MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC57qC/BeoYcM6ijazuaCdJkbT8pvPpFEDVzf9ZQ9axswXU3mywSOaR3wflriSjmvRfUNs/BAjshgtJqgviUXx7lE5aG9mcUyvomyFFpfCR2l2Lvow0H8y7JoL6yxMSQf8gpAcaQzPB8dsfGe+DqA+5wjxXPOhC1QUcllt08yBB3wIDAQAB" Signature string = "lsOsGOrY0rrs4A2CaJ3FzKLU5jx41d/Dw7gxQLUDPC4KMq6Cd3hyjZN6B8BbCDHBcZCFSd+sKvUbmM+ZCM1D6OrqYGvoRLTZJjWqbUsHRS7PkmIUWToxWxe0qo+tq5K/aYoDPJ+o6fRYTnUGILkN5+pQ8NquJqviLPCvBJVpKCo=" ) @@ -62,27 +62,27 @@ func TestValidateSignature(t *testing.T) { var req Request var hashData = "foobar data test" - req = NewRequest("GET", "/", "") + req = NewRequest("GET", "/", "", nil) req.Headers.Set("authorization", "Bearer "+Signature) assert.True(t, req.ValidateAuthenticationToken(PubKeyData, hashData)) - req = NewRequest("GET", "/", "") + req = NewRequest("GET", "/", "", nil) req.Headers.Set("authorization", "Bearer "+Signature) assert.False(t, req.ValidateAuthenticationToken("false data", hashData)) - req = NewRequest("GET", "/", "") + req = NewRequest("GET", "/", "", nil) req.Headers.Set("authorization", "Bearer "+Signature) assert.False(t, req.ValidateAuthenticationToken(PubKeyData, hashData+"falsefalse")) - req = NewRequest("GET", "/", "") + req = NewRequest("GET", "/", "", nil) req.Headers.Set("authorization", "ADSAFAFAF") assert.False(t, req.ValidateAuthenticationToken(PubKeyData, hashData)) - req = NewRequest("GET", "/", "") + req = NewRequest("GET", "/", "", nil) req.Headers.Set("authorization", "Bearer *&^(&^%(^&#@%$%)@$%@!$^@$^)@!") assert.False(t, req.ValidateAuthenticationToken(PubKeyData, hashData)) - req = NewRequest("GET", "/", "") + req = NewRequest("GET", "/", "", nil) req.Headers.Set("authorization", "") assert.False(t, req.ValidateAuthenticationToken(PubKeyData, hashData)) } @@ -91,3 +91,10 @@ func TestMain(m *testing.M) { log.SetOutput(ioutil.Discard) os.Exit(m.Run()) } + +func TestGenerateAuthenticationToken(t *testing.T) { + priv, _, _ := testing2.ReadTestKey("../../testdata/key-1.json") + + token := GenerateAuthenticationToken([]byte("secret"), *priv) + assert.Equal(t, "RHurMX2K6xiBrfYuyWufGegfrTArrn9Nm/MJaCswwqEpV3HTaQaeEEcQefM5RyzQoF4UIbPvHxRrbjL8u9Nns8GvpZ/ACdDN3MXOX0zVjkydX4Iit0k32PfikzX1kFvM0B7Lak7iNUoq0KMacBJ6ri+v+SCSSwvukB5dO5y4zdIOU1Dfypel62gc58+FWyIDcoVQEjb+hpAs1CVd5wNMR4iMe6sovp2JQ4FMVd0LEJLDOcfGHtv0kg+jikSt+QmR5YuKwIfjxZHA/dPkyL6bMmwizap4CfF/qBbiGADxkPQIxmPxuZ7mSPrtukIJu1DHayhbcp19ikfKvG8fBziLMg==", token) +} diff --git a/internal/logo.go b/internal/logo.go index f0acf2b..a48d7af 100644 --- a/internal/logo.go +++ b/internal/logo.go @@ -19,10 +19,10 @@ package internal -var Version = "0.1.0" +var GitCommit = "dev" var Logo = " ____ _ _ __ __ _\n" + - "| _ \\(_) | | \\/ | | | " + Version + "\n" + + "| _ \\(_) | | \\/ | | | " + GitCommit + "\n" + "| |_) |_| |_| \\ / | __ _ ___| |_ _ _ __ ___\n" + "| _ <| | __| |\\/| |/ _` |/ _ \\ | | | | '_ ` _ \\\n" + "| |_) | | |_| | | | (_| | __/ | |_| | | | | | |\n" + diff --git a/internal/organisation/bolt.go b/internal/organisation/bolt.go index f8c3555..f195749 100644 --- a/internal/organisation/bolt.go +++ b/internal/organisation/bolt.go @@ -24,7 +24,7 @@ import ( "time" "github.com/bitmaelum/key-resolver-go/internal" - "github.com/boltdb/bolt" + bolt "go.etcd.io/bbolt" ) type boltResolver struct { @@ -77,6 +77,8 @@ func (b boltResolver) Create(hash, publicKey, proof string, validations []string Proof: proof, Validations: validations, Serial: uint64(time.Now().UnixNano()), + Deleted: false, + DeletedAt: time.Time{}, } buf, err := json.Marshal(rec) if err != nil { @@ -113,3 +115,80 @@ func (b boltResolver) Delete(hash string) (bool, error) { return true, nil } + +func (b boltResolver) SoftDelete(hash string) (bool, error) { + err := b.client.Update(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(b.bucketName)) + if bucket == nil { + return nil + } + + rec, err := getFromBucket(bucket, hash) + if err != nil { + return ErrNotFound + } + + // make record deleted + rec.Deleted = true + rec.DeletedAt = time.Now() + + // Store + buf, err := json.Marshal(rec) + if err != nil { + return err + } + return bucket.Put([]byte(hash), buf) + }) + + if err != nil { + return false, err + } + + return true, nil +} + +func (b boltResolver) SoftUndelete(hash string) (bool, error) { + err := b.client.Update(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(b.bucketName)) + if bucket == nil { + return nil + } + + rec, err := getFromBucket(bucket, hash) + if err != nil { + return ErrNotFound + } + + // undelete + rec.Deleted = false + rec.DeletedAt = time.Time{} + + // Store + buf, err := json.Marshal(rec) + if err != nil { + return err + } + return bucket.Put([]byte(hash), buf) + }) + + if err != nil { + return false, err + } + + return true, nil +} + +func getFromBucket(bucket *bolt.Bucket, hash string) (*ResolveInfoType, error) { + data := bucket.Get([]byte(hash)) + if data == nil { + return nil, ErrNotFound + } + + rec := &ResolveInfoType{} + err := json.Unmarshal(data, &rec) + if err != nil { + return nil, ErrNotFound + } + + return rec, nil +} diff --git a/internal/organisation/dynamodb.go b/internal/organisation/dynamodb.go index 6b7640b..085ecaa 100644 --- a/internal/organisation/dynamodb.go +++ b/internal/organisation/dynamodb.go @@ -31,7 +31,7 @@ import ( ) type dynamoDbResolver struct { - C *dynamodb.DynamoDB + Dyna *dynamodb.DynamoDB TableName string } @@ -45,12 +45,14 @@ type Record struct { Proof string `dynamodbav:"proof"` Validations []string `dynamodbav:"validations"` Serial uint64 `dynamodbav:"sn"` + Deleted bool `dynamodbav:"deleted"` + DeletedAt uint64 `dynamodbav:"deleted_at"` } // NewDynamoDBResolver returns a new resolver based on DynamoDB func NewDynamoDBResolver(client *dynamodb.DynamoDB, tableName string) Repository { return &dynamoDbResolver{ - C: client, + Dyna: client, TableName: tableName, } } @@ -74,7 +76,7 @@ func (r *dynamoDbResolver) Update(info *ResolveInfoType, publicKey, proof string }, } - _, err := r.C.UpdateItem(input) + _, err := r.Dyna.UpdateItem(input) if err != nil { log.Print(err) return false, err @@ -103,12 +105,12 @@ func (r *dynamoDbResolver) Create(hash, publicKey, proof string, validations []s TableName: aws.String(r.TableName), } - _, err = r.C.PutItem(input) + _, err = r.Dyna.PutItem(input) return err == nil, err } func (r *dynamoDbResolver) Get(hash string) (*ResolveInfoType, error) { - result, err := r.C.GetItem(&dynamodb.GetItemInput{ + result, err := r.Dyna.GetItem(&dynamodb.GetItemInput{ TableName: aws.String(r.TableName), Key: map[string]*dynamodb.AttributeValue{ "hash": {S: aws.String(hash)}, @@ -132,6 +134,11 @@ func (r *dynamoDbResolver) Get(hash string) (*ResolveInfoType, error) { return nil, ErrNotFound } + // We would prefer if we didn't retrieve it from the Getitem input + if record.Deleted { + return nil, ErrNotFound + } + return &ResolveInfoType{ Hash: record.Hash, PubKey: record.PublicKey, @@ -142,7 +149,7 @@ func (r *dynamoDbResolver) Get(hash string) (*ResolveInfoType, error) { } func (r *dynamoDbResolver) Delete(hash string) (bool, error) { - _, err := r.C.DeleteItem(&dynamodb.DeleteItemInput{ + _, err := r.Dyna.DeleteItem(&dynamodb.DeleteItemInput{ TableName: aws.String(r.TableName), Key: map[string]*dynamodb.AttributeValue{ "hash": {S: aws.String(hash)}, @@ -157,3 +164,45 @@ func (r *dynamoDbResolver) Delete(hash string) (bool, error) { return true, nil } + +func (r *dynamoDbResolver) SoftDelete(hash string) (bool, error) { + input := &dynamodb.UpdateItemInput{ + ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{ + ":dt": {N: aws.String(strconv.FormatInt(time.Now().Unix(), 10))}, + }, + TableName: aws.String(r.TableName), + UpdateExpression: aws.String("SET deleted=1, deleted_at=:dt"), + Key: map[string]*dynamodb.AttributeValue{ + "hash": {S: aws.String(hash)}, + }, + } + + _, err := r.Dyna.UpdateItem(input) + if err != nil { + log.Print(err) + return false, err + } + + return true, nil +} + +func (r *dynamoDbResolver) SoftUndelete(hash string) (bool, error) { + input := &dynamodb.UpdateItemInput{ + ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{ + ":dt": {N: aws.String("")}, + }, + TableName: aws.String(r.TableName), + UpdateExpression: aws.String("SET deleted=0, deleted_at=:dt"), + Key: map[string]*dynamodb.AttributeValue{ + "hash": {S: aws.String(hash)}, + }, + } + + _, err := r.Dyna.UpdateItem(input) + if err != nil { + log.Print(err) + return false, err + } + + return true, nil +} diff --git a/internal/organisation/repository.go b/internal/organisation/repository.go index b71e503..8114925 100644 --- a/internal/organisation/repository.go +++ b/internal/organisation/repository.go @@ -21,6 +21,7 @@ package organisation import ( "os" + "time" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/dynamodb" @@ -33,6 +34,8 @@ type ResolveInfoType struct { Proof string Validations []string Serial uint64 + Deleted bool + DeletedAt time.Time } // Repository to resolve records @@ -40,6 +43,8 @@ type Repository interface { Get(hash string) (*ResolveInfoType, error) Create(hash, publicKey, proof string, validations []string) (bool, error) Update(info *ResolveInfoType, publicKey, proof string, validations []string) (bool, error) + SoftDelete(hash string) (bool, error) + SoftUndelete(hash string) (bool, error) Delete(hash string) (bool, error) } diff --git a/internal/organisation/sqlite.go b/internal/organisation/sqlite.go index 2739aff..a42f786 100644 --- a/internal/organisation/sqlite.go +++ b/internal/organisation/sqlite.go @@ -38,7 +38,7 @@ type SqliteDbResolver struct { } // NewDynamoDBResolver returns a new resolver based on DynamoDB -func NewSqliteResolver(dsn string) *SqliteDbResolver { +func NewSqliteResolver(dsn string) Repository { if !strings.HasPrefix(dsn, "file:") { if dsn == ":memory:" { dsn = "file::memory:?mode=memory" @@ -58,7 +58,7 @@ func NewSqliteResolver(dsn string) *SqliteDbResolver { TimeNow: time.Now(), } - _, _ = db.conn.Exec("CREATE TABLE IF NOT EXISTS mock_organisation (hash VARCHAR(64) PRIMARY KEY, proof TEXT, validations TEXT, pubkey TEXT, serial INTEGER)") + _, _ = db.conn.Exec("CREATE TABLE IF NOT EXISTS mock_organisation (hash VARCHAR(64) PRIMARY KEY, proof TEXT, validations TEXT, pubkey TEXT, serial INTEGER, deleted INTEGER, deleted_at INTEGER)") return db } @@ -92,7 +92,7 @@ func (r *SqliteDbResolver) Create(hash, publicKey, proof string, validations []s return false, err } - res, err := r.conn.Exec("INSERT INTO mock_organisation VALUES (?, ?, ?, ?, ?)", hash, proof, string(b), publicKey, newSerial) + res, err := r.conn.Exec("INSERT INTO mock_organisation VALUES (?, ?, ?, ?, ?, 0, 0)", hash, proof, string(b), publicKey, newSerial) if err != nil { return false, err } @@ -140,3 +140,35 @@ func (r *SqliteDbResolver) Delete(hash string) (bool, error) { count, err := res.RowsAffected() return count != 0, err } + +func (r *SqliteDbResolver) SoftDelete(hash string) (bool, error) { + st, err := r.conn.Prepare("UPDATE mock_organisation SET deleted=1, deleted_at=? WHERE hash=?") + if err != nil { + return false, err + } + + dt := time.Now().Unix() + res, err := st.Exec(dt, hash) + if err != nil { + return false, err + } + + count, err := res.RowsAffected() + return count != 0, err + +} + +func (r *SqliteDbResolver) SoftUndelete(hash string) (bool, error) { + st, err := r.conn.Prepare("UPDATE mock_organisation SET deleted=0, deleted_at=NULL WHERE hash=?") + if err != nil { + return false, err + } + + res, err := st.Exec(hash) + if err != nil { + return false, err + } + + count, err := res.RowsAffected() + return count != 0, err +} diff --git a/internal/routing/boltdb.go b/internal/routing/boltdb.go index 9c89c1f..6232de7 100644 --- a/internal/routing/boltdb.go +++ b/internal/routing/boltdb.go @@ -24,7 +24,7 @@ import ( "time" "github.com/bitmaelum/key-resolver-go/internal" - "github.com/boltdb/bolt" + bolt "go.etcd.io/bbolt" ) type boltResolver struct { diff --git a/internal/testing/testing_test.go b/internal/testing/testing_test.go new file mode 100644 index 0000000..ba776f8 --- /dev/null +++ b/internal/testing/testing_test.go @@ -0,0 +1,38 @@ +// Copyright (c) 2020 BitMaelum Authors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package testing + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestReadTestKey(t *testing.T) { + priv, pub, err := ReadTestKey("../../testdata/key-7.json") + assert.NoError(t, err) + assert.Equal(t, priv.String(), "ed25519 MC4CAQAwBQYDK2VwBCIEIApsDq5uwKSUNlmw9z3u63CeNdrfDgBOkJRmvM6gvQj3") + assert.Equal(t, pub.String(), "ed25519 MCowBQYDK2VwAyEA1xbVcwtwUx9EFnvZltYd7qz1FxwJOOugkkA9vHYxoQM=") + + priv, pub, err = ReadTestKey("../does-not-exist.json") + assert.Error(t, err) + assert.Nil(t, priv) + assert.Nil(t, pub) +}