From 2d036d344a788b55b8d3a6c10c131f65d645c4bf Mon Sep 17 00:00:00 2001 From: Gabe Date: Tue, 19 Apr 2022 17:23:58 -0700 Subject: [PATCH] VC JSON Schema Support (#23) * temp * shuffle tests * update deps * temp * Schema handlers * bad renames * temp * update deps * schema test * structure for schema http test * schema tests --- .github/workflows/codeql-analysis.yml | 13 +- go.mod | 23 +- go.sum | 48 +++-- internal/util/util.go | 15 ++ pkg/server/router/did.go | 55 ++--- pkg/server/router/did_test.go | 24 +-- pkg/server/router/schema.go | 102 +++++++++ pkg/server/router/schema_test.go | 107 ++++++++++ pkg/server/server.go | 22 +- pkg/server/server_test.go | 265 +++++++++++++++++++----- pkg/service/did/did.go | 34 ++- pkg/service/did/key.go | 9 +- pkg/service/did/{models.go => model.go} | 12 +- pkg/service/did/storage/bolt.go | 22 +- pkg/service/did/storage/storage.go | 4 +- pkg/service/framework/framework.go | 5 +- pkg/service/schema/model.go | 30 +++ pkg/service/schema/schema.go | 101 +++++++++ pkg/service/schema/storage/bolt.go | 76 +++++++ pkg/service/schema/storage/storage.go | 32 +++ pkg/service/service.go | 13 +- pkg/storage/bolt.go | 20 +- pkg/storage/bolt_test.go | 17 +- 23 files changed, 858 insertions(+), 191 deletions(-) create mode 100644 internal/util/util.go create mode 100644 pkg/server/router/schema.go create mode 100644 pkg/server/router/schema_test.go rename pkg/service/did/{models.go => model.go} (65%) create mode 100644 pkg/service/schema/model.go create mode 100644 pkg/service/schema/schema.go create mode 100644 pkg/service/schema/storage/bolt.go create mode 100644 pkg/service/schema/storage/storage.go diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index b7b971094..ebbfeefeb 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -28,6 +28,8 @@ jobs: actions: read contents: read security-events: write + env: + GOFLAGS: "-tags=jwx_es256k" strategy: fail-fast: false @@ -55,16 +57,5 @@ jobs: - name: Autobuild uses: github/codeql-action/autobuild@v2 - # ℹī¸ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # ✏ī¸ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 diff --git a/go.mod b/go.mod index ef0550f14..ecf7e3433 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/tbd54566975/ssi-service go 1.17 require ( - github.com/TBD54566975/ssi-sdk v0.0.0-20220403173431-39ff8ebd3825 + github.com/TBD54566975/ssi-sdk v0.0.0-20220419032604-6292424ef7ae github.com/ardanlabs/conf v1.5.0 github.com/boltdb/bolt v1.3.1 github.com/dimfeld/httptreemux/v5 v5.4.0 @@ -15,25 +15,25 @@ require ( github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.7.1 go.opentelemetry.io/otel/trace v1.6.3 - golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 + golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 gopkg.in/go-playground/validator.v9 v9.31.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect - github.com/go-playground/validator/v10 v10.10.0 // indirect + github.com/go-playground/validator/v10 v10.10.1 // indirect github.com/gobuffalo/logger v1.0.6 // indirect github.com/gobuffalo/packd v1.0.1 // indirect github.com/gobuffalo/packr/v2 v2.8.3 // indirect - github.com/goccy/go-json v0.9.4 // indirect + github.com/goccy/go-json v0.9.6 // indirect github.com/karrick/godirwalk v1.16.1 // indirect github.com/leodido/go-urn v1.2.1 // indirect github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect github.com/lestrrat-go/blackmagic v1.0.0 // indirect - github.com/lestrrat-go/httpcc v1.0.0 // indirect + github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/iter v1.0.1 // indirect - github.com/lestrrat-go/jwx v1.2.19 // indirect + github.com/lestrrat-go/jwx v1.2.23 // indirect github.com/lestrrat-go/option v1.0.0 // indirect github.com/markbates/errx v1.1.0 // indirect github.com/markbates/oncer v1.0.0 // indirect @@ -41,16 +41,19 @@ require ( github.com/multiformats/go-base32 v0.0.3 // indirect github.com/multiformats/go-base36 v0.1.0 // indirect github.com/multiformats/go-multibase v0.0.3 // indirect - github.com/multiformats/go-multicodec v0.4.0 // indirect + github.com/multiformats/go-multicodec v0.4.1 // indirect github.com/multiformats/go-varint v0.0.6 // indirect github.com/piprate/json-gold v0.4.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect github.com/sirupsen/logrus v1.8.1 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect go.opentelemetry.io/otel v1.6.3 // indirect - golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f // indirect - golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect - golang.org/x/text v0.3.6 // indirect + golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect + golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 // indirect + golang.org/x/text v0.3.7 // indirect gopkg.in/go-playground/assert.v1 v1.2.1 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect ) diff --git a/go.sum b/go.sum index f37c43355..82c0422fa 100644 --- a/go.sum +++ b/go.sum @@ -39,8 +39,8 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/TBD54566975/ssi-sdk v0.0.0-20220403173431-39ff8ebd3825 h1:IRup2wdgV5IvPZKx+XlCbJVDy28Pa60rxdA147NuE5I= -github.com/TBD54566975/ssi-sdk v0.0.0-20220403173431-39ff8ebd3825/go.mod h1:6dpqc6LQ8EQVhZvrk1beTosnLqD5iR7jEorZHLpKUF0= +github.com/TBD54566975/ssi-sdk v0.0.0-20220419032604-6292424ef7ae h1:8f4RIwlopUS2hmj1ohvRjVnftZGVQzubNenZb/9q+sY= +github.com/TBD54566975/ssi-sdk v0.0.0-20220419032604-6292424ef7ae/go.mod h1:dZf0M3fNzzqFH31ePuA8XfWQTS0RKEn6cDSjcwS+Ls0= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/ardanlabs/conf v1.5.0 h1:5TwP6Wu9Xi07eLFEpiCUF3oQXh9UzHMDVnD3u/I5d5c= github.com/ardanlabs/conf v1.5.0/go.mod h1:ILsMo9dMqYzCxDjDXTiwMI0IgxOJd0MOiucbQY2wlJw= @@ -100,8 +100,8 @@ github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= -github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0= -github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= +github.com/go-playground/validator/v10 v10.10.1 h1:uA0+amWMiglNZKZ9FJRKUAe9U3RX91eVn1JYXMWt7ig= +github.com/go-playground/validator/v10 v10.10.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= github.com/gobuffalo/logger v1.0.0/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs= github.com/gobuffalo/logger v1.0.6 h1:nnZNpxYo0zx+Aj9RfMPBm+x9zAU2OayFh/xrAWi34HU= @@ -114,8 +114,8 @@ github.com/gobuffalo/packr v1.30.1/go.mod h1:ljMyFO2EcrnzsHsN99cvbq055Y9OhRrIavi github.com/gobuffalo/packr/v2 v2.5.1/go.mod h1:8f9c96ITobJlPzI44jj+4tHnEKNt0xXWSVlXRN9X1Iw= github.com/gobuffalo/packr/v2 v2.8.3 h1:xE1yzvnO56cUC0sTpKR3DIbxZgB54AftTFMhB2XEWlY= github.com/gobuffalo/packr/v2 v2.8.3/go.mod h1:0SahksCVcx4IMnigTjiFuyldmTrdTctXsOdiU5KwbKc= -github.com/goccy/go-json v0.9.4 h1:L8MLKG2mvVXiQu07qB6hmfqeSYQdOnqPot2GhsIwIaI= -github.com/goccy/go-json v0.9.4/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-json v0.9.6 h1:5/4CtRQdtsX0sal8fdVhTaiMN01Ri8BExZZ8iRmHQ6E= +github.com/goccy/go-json v0.9.6/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -235,12 +235,12 @@ github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= github.com/lestrrat-go/blackmagic v1.0.0 h1:XzdxDbuQTz0RZZEmdU7cnQxUtFUzgCSPq8RCz4BxIi4= github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ= -github.com/lestrrat-go/httpcc v1.0.0 h1:FszVC6cKfDvBKcJv646+lkh4GydQg2Z29scgUfkOpYc= -github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++0Gf8MBnAvE= +github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= github.com/lestrrat-go/iter v1.0.1 h1:q8faalr2dY6o8bV45uwrxq12bRa1ezKrB6oM9FUgN4A= github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc= -github.com/lestrrat-go/jwx v1.2.19 h1:qxxLmAXNwZpTTvjc4PH21nT7I4wPK6lVv3lVNcZPnUk= -github.com/lestrrat-go/jwx v1.2.19/go.mod h1:bWTBO7IHHVMtNunM8so9MT8wD+euEY1PzGEyCnuI2qM= +github.com/lestrrat-go/jwx v1.2.23 h1:8oP5fY1yzCRraUNNyfAVdOkLCqY7xMZz11lVcvHqC1Y= +github.com/lestrrat-go/jwx v1.2.23/go.mod h1:sAXjRwzSvCN6soO4RLoWWm1bVPpb8iOuv0IYfH8OWd8= github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4= github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/magefile/mage v1.13.0 h1:XtLJl8bcCM7EFoO8FyH8XK3t7G5hQAeK+i4tq+veT9M= @@ -278,8 +278,8 @@ github.com/multiformats/go-base36 v0.1.0 h1:JR6TyF7JjGd3m6FbLU2cOxhC0Li8z8dLNGQ8 github.com/multiformats/go-base36 v0.1.0/go.mod h1:kFGE83c6s80PklsHO9sRn2NCoffoRdUUOENyW/Vv6sM= github.com/multiformats/go-multibase v0.0.3 h1:l/B6bJDQjvQ5G52jw4QGSYeOTZoAwIO77RblWplfIqk= github.com/multiformats/go-multibase v0.0.3/go.mod h1:5+1R4eQrT3PkYZ24C3W2Ue2tPwIdYQD509ZjSb5y9Oc= -github.com/multiformats/go-multicodec v0.4.0 h1:fbqb6ky7erjdD+/zaEBJgZWu1i8D6i/wmPywGK7sdow= -github.com/multiformats/go-multicodec v0.4.0/go.mod h1:1Hj/eHRaVWSXiSNNfcEPcwZleTmdNP81xlxDLnWU9GQ= +github.com/multiformats/go-multicodec v0.4.1 h1:BSJbf+zpghcZMZrwTYBGwy0CPcVZGWiC72Cp8bBd4R4= +github.com/multiformats/go-multicodec v0.4.1/go.mod h1:1Hj/eHRaVWSXiSNNfcEPcwZleTmdNP81xlxDLnWU9GQ= github.com/multiformats/go-varint v0.0.6 h1:gk85QWKxh3TazbLxED/NlDVv8+q+ReFJk7Y2W/KhfNY= github.com/multiformats/go-varint v0.0.6/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852/go.mod h1:eqOVx5Vwu4gd2mmMZvVZsgIqNSaW3xxRThUJ0k/TPk4= @@ -339,8 +339,11 @@ github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMT github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -375,11 +378,10 @@ golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201217014255-9d1352758620/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o= -golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA= +golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -526,13 +528,12 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f h1:rlezHXNlxYWvBCzNses9Dlc7nGFaNMJeqLolcmQSSZY= -golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 h1:EH1Deb8WZJ0xc0WK//leUHXcX9aLE5SymusoTmMZye8= +golang.org/x/term v0.0.0-20220411215600-e5f449aeb171/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -540,8 +541,9 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/internal/util/util.go b/internal/util/util.go new file mode 100644 index 000000000..0fed26c6c --- /dev/null +++ b/internal/util/util.go @@ -0,0 +1,15 @@ +package util + +import ( + "fmt" + "strings" +) + +// GetMethodForDID gets a DID method from a did, the second part of the did (e.g. did:test:abcd, the method is 'test') +func GetMethodForDID(did string) (string, error) { + split := strings.Split(did, ":") + if len(split) < 3 { + return "", fmt.Errorf("malformed did: %s", did) + } + return split[1], nil +} diff --git a/pkg/server/router/did.go b/pkg/server/router/did.go index 3507fd0d1..4c527293a 100644 --- a/pkg/server/router/did.go +++ b/pkg/server/router/did.go @@ -20,8 +20,8 @@ const ( // DIDRouter represents the dependencies required to instantiate a DID-HTTP service type DIDRouter struct { - did.Service - *log.Logger + service *did.Service + logger *log.Logger } // NewDIDRouter creates an HTP router for the DID Service @@ -34,8 +34,8 @@ func NewDIDRouter(s svcframework.Service, l *log.Logger) (*DIDRouter, error) { return nil, fmt.Errorf("could not create DID router with service type: %s", s.Type()) } return &DIDRouter{ - Service: *didService, - Logger: l, + service: didService, + logger: l, }, nil } @@ -43,9 +43,9 @@ type GetDIDMethodsResponse struct { DIDMethods []did.Method `json:"didMethods,omitempty"` } -func (s DIDRouter) GetDIDMethods(ctx context.Context, w http.ResponseWriter, _ *http.Request) error { - methods := s.GetSupportedMethods() - response := GetDIDMethodsResponse{DIDMethods: methods} +func (dr DIDRouter) GetDIDMethods(ctx context.Context, w http.ResponseWriter, _ *http.Request) error { + methods := dr.service.GetSupportedMethods() + response := GetDIDMethodsResponse{DIDMethods: methods.Methods} return framework.Respond(ctx, w, response, http.StatusOK) } @@ -58,34 +58,27 @@ type CreateDIDByMethodResponse struct { PrivateKey string `json:"privateKeyBase58,omitempty"` } -func (s DIDRouter) CreateDIDByMethod(ctx context.Context, w http.ResponseWriter, r *http.Request) error { +func (dr DIDRouter) CreateDIDByMethod(ctx context.Context, w http.ResponseWriter, r *http.Request) error { method := framework.GetParam(ctx, MethodParam) if method == nil { errMsg := "create DID request missing method parameter" - s.Logger.Printf(errMsg) + dr.logger.Printf(errMsg) return framework.NewRequestErrorMsg(errMsg, http.StatusBadRequest) } var request CreateDIDByMethodRequest if err := framework.Decode(r, &request); err != nil { errMsg := "invalid create DID request" - s.Logger.Printf(errMsg) - return framework.NewRequestErrorMsg(errMsg, http.StatusBadRequest) - } - - // TODO(gabe) check if the method is supported, to tell whether this is a bad req or internal error - handler, err := s.GetHandler(did.Method(*method)) - if err != nil { - errMsg := fmt.Sprintf("could not get handler for method<%s>", *method) - s.Logger.Printf(errMsg) + dr.logger.Printf(errors.Wrap(err, errMsg).Error()) return framework.NewRequestErrorMsg(errMsg, http.StatusBadRequest) } // TODO(gabe) check if the key type is supported for the method, to tell whether this is a bad req or internal error - createDIDResponse, err := handler.CreateDID(did.CreateDIDRequest{KeyType: request.KeyType}) + createDIDRequest := did.CreateDIDRequest{Method: did.Method(*method), KeyType: request.KeyType} + createDIDResponse, err := dr.service.CreateDIDByMethod(createDIDRequest) if err != nil { errMsg := fmt.Sprintf("could not create DID for method<%s> with key type: %s", *method, request.KeyType) - s.Logger.Printf(errMsg) + dr.logger.Printf(errMsg) return framework.NewRequestErrorMsg(errMsg, http.StatusInternalServerError) } @@ -93,41 +86,35 @@ func (s DIDRouter) CreateDIDByMethod(ctx context.Context, w http.ResponseWriter, DID: createDIDResponse.DID, PrivateKey: createDIDResponse.PrivateKey, } - return framework.Respond(ctx, w, resp, http.StatusOK) + return framework.Respond(ctx, w, resp, http.StatusCreated) } type GetDIDByMethodResponse struct { DID didsdk.DIDDocument `json:"did,omitempty"` } -func (s DIDRouter) GetDIDByMethod(ctx context.Context, w http.ResponseWriter, _ *http.Request) error { +func (dr DIDRouter) GetDIDByMethod(ctx context.Context, w http.ResponseWriter, _ *http.Request) error { method := framework.GetParam(ctx, MethodParam) if method == nil { errMsg := "get DID by method request missing method parameter" - s.Logger.Printf(errMsg) + dr.logger.Printf(errMsg) return framework.NewRequestErrorMsg(errMsg, http.StatusBadRequest) } id := framework.GetParam(ctx, IDParam) if id == nil { errMsg := fmt.Sprintf("get DID request missing id parameter for method: %s", *method) - s.Logger.Printf(errMsg) + dr.logger.Printf(errMsg) return framework.NewRequestErrorMsg(errMsg, http.StatusBadRequest) } // TODO(gabe) check if the method is supported, to tell whether this is a bad req or internal error - handler, err := s.GetHandler(did.Method(*method)) - if err != nil { - errMsg := fmt.Sprintf("could not get handler for method<%s>", *method) - s.Logger.Printf(errMsg) - return framework.NewRequestErrorMsg(errMsg, http.StatusBadRequest) - } - // TODO(gabe) differentiate between internal errors and not found DIDs - gotDID, err := handler.GetDID(*id) + getDIDRequest := did.GetDIDRequest{Method: did.Method(*method), ID: *id} + gotDID, err := dr.service.GetDIDByMethod(getDIDRequest) if err != nil { errMsg := fmt.Sprintf("could not get DID for method<%s> with id: %s", *method, *id) - s.Logger.Printf(errMsg) - return framework.NewRequestErrorMsg(errMsg, http.StatusNotFound) + dr.logger.Printf(errors.Wrap(err, errMsg).Error()) + return framework.NewRequestErrorMsg(errMsg, http.StatusBadRequest) } resp := GetDIDByMethodResponse{DID: gotDID.DID} diff --git a/pkg/server/router/did_test.go b/pkg/server/router/did_test.go index 0fa363cc6..d21e47131 100644 --- a/pkg/server/router/did_test.go +++ b/pkg/server/router/did_test.go @@ -33,11 +33,12 @@ func TestDIDRouter(t *testing.T) { }) t.Run("DID Service Test", func(tt *testing.T) { - bolt, err := storage.NewBoltDB() + logger := log.New(os.Stdout, "ssi-test", log.LstdFlags) + + bolt, err := storage.NewBoltDB(logger) assert.NoError(tt, err) assert.NotEmpty(tt, bolt) - logger := log.New(os.Stdout, "ssi-test", log.LstdFlags) didService, err := did.NewDIDService(logger, []did.Method{did.KeyMethod}, bolt) assert.NoError(tt, err) assert.NotEmpty(tt, didService) @@ -47,27 +48,22 @@ func TestDIDRouter(t *testing.T) { assert.Equal(tt, framework.StatusReady, didService.Status().Status) // get unknown handler - _, err = didService.GetHandler("bad") + _, err = didService.GetDIDByMethod(did.GetDIDRequest{Method: "bad"}) assert.Error(tt, err) - assert.Contains(tt, err.Error(), "could not get handler for DID method: bad") + assert.Contains(tt, err.Error(), "could not get handler for method") supported := didService.GetSupportedMethods() assert.NotEmpty(tt, supported) - assert.Len(tt, supported, 1) - assert.Equal(tt, did.KeyMethod, supported[0]) - - // get known handler - keyHandler, err := didService.GetHandler(did.KeyMethod) - assert.NoError(tt, err) - assert.NotEmpty(tt, keyHandler) + assert.Len(tt, supported.Methods, 1) + assert.Equal(tt, did.KeyMethod, supported.Methods[0]) // bad key type - _, err = keyHandler.CreateDID(did.CreateDIDRequest{KeyType: "bad"}) + _, err = didService.CreateDIDByMethod(did.CreateDIDRequest{Method: did.KeyMethod, KeyType: "bad"}) assert.Error(tt, err) assert.Contains(tt, err.Error(), "could not create did:key") // good key type - createDIDResponse, err := keyHandler.CreateDID(did.CreateDIDRequest{KeyType: crypto.Ed25519}) + createDIDResponse, err := didService.CreateDIDByMethod(did.CreateDIDRequest{Method: did.KeyMethod, KeyType: crypto.Ed25519}) assert.NoError(tt, err) assert.NotEmpty(tt, createDIDResponse) @@ -75,7 +71,7 @@ func TestDIDRouter(t *testing.T) { assert.Contains(tt, createDIDResponse.DID.ID, "did:key") // get it back - getDIDResponse, err := keyHandler.GetDID(createDIDResponse.DID.ID) + getDIDResponse, err := didService.GetDIDByMethod(did.GetDIDRequest{Method: did.KeyMethod, ID: createDIDResponse.DID.ID}) assert.NoError(tt, err) assert.NotEmpty(tt, getDIDResponse) diff --git a/pkg/server/router/schema.go b/pkg/server/router/schema.go new file mode 100644 index 000000000..c6eee4be5 --- /dev/null +++ b/pkg/server/router/schema.go @@ -0,0 +1,102 @@ +package router + +import ( + "context" + "fmt" + schemalib "github.com/TBD54566975/ssi-sdk/credential/schema" + "github.com/pkg/errors" + "github.com/tbd54566975/ssi-service/pkg/server/framework" + svcframework "github.com/tbd54566975/ssi-service/pkg/service/framework" + "github.com/tbd54566975/ssi-service/pkg/service/schema" + "log" + "net/http" +) + +type SchemaRouter struct { + service *schema.Service + logger *log.Logger +} + +func NewSchemaRouter(s svcframework.Service, l *log.Logger) (*SchemaRouter, error) { + if s == nil { + return nil, errors.New("service cannot be nil") + } + schemaService, ok := s.(*schema.Service) + if !ok { + return nil, fmt.Errorf("could not create schema router with service type: %s", s.Type()) + } + return &SchemaRouter{ + service: schemaService, + logger: l, + }, nil +} + +type CreateSchemaRequest struct { + Author string `json:"author" validate:"required"` + Name string `json:"name" validate:"required"` + Schema schemalib.JSONSchema `json:"schema" validate:"required"` +} + +type CreateSchemaResponse struct { + ID string `json:"id"` + Schema schemalib.VCJSONSchema `json:"schema"` +} + +func (sr SchemaRouter) CreateSchema(ctx context.Context, w http.ResponseWriter, r *http.Request) error { + var request CreateSchemaRequest + if err := framework.Decode(r, &request); err != nil { + errMsg := "invalid create schema request" + sr.logger.Printf(errors.Wrap(err, errMsg).Error()) + return framework.NewRequestErrorMsg(errMsg, http.StatusBadRequest) + } + + req := schema.CreateSchemaRequest{Author: request.Author, Name: request.Name, Schema: request.Schema} + createSchemaResponse, err := sr.service.CreateSchema(req) + if err != nil { + errMsg := fmt.Sprintf("could not create schema with authoring DID: %s", request.Author) + sr.logger.Printf(errMsg) + return framework.NewRequestErrorMsg(errMsg, http.StatusInternalServerError) + } + + resp := CreateSchemaResponse{ID: createSchemaResponse.ID, Schema: createSchemaResponse.Schema} + return framework.Respond(ctx, w, resp, http.StatusCreated) +} + +type GetSchemasResponse struct { + Schemas []schemalib.VCJSONSchema `json:"schemas,omitempty"` +} + +func (sr SchemaRouter) GetSchemas(ctx context.Context, w http.ResponseWriter, r *http.Request) error { + gotSchemas, err := sr.service.GetSchemas() + if err != nil { + errMsg := "could not get schemas" + sr.logger.Printf(errors.Wrap(err, errMsg).Error()) + return framework.NewRequestErrorMsg(errMsg, http.StatusInternalServerError) + } + resp := GetSchemasResponse{Schemas: gotSchemas.Schemas} + return framework.Respond(ctx, w, resp, http.StatusOK) +} + +type GetSchemaResponse struct { + Schema schemalib.VCJSONSchema `json:"schema,omitempty"` +} + +func (sr SchemaRouter) GetSchemaByID(ctx context.Context, w http.ResponseWriter, r *http.Request) error { + id := framework.GetParam(ctx, IDParam) + if id == nil { + errMsg := "cannot get schema without ID parameter" + sr.logger.Printf(errMsg) + return framework.NewRequestErrorMsg(errMsg, http.StatusBadRequest) + } + + // TODO(gabe) differentiate between internal errors and not found schemas + gotSchema, err := sr.service.GetSchemaByID(schema.GetSchemaByIDRequest{ID: *id}) + if err != nil { + errMsg := fmt.Sprintf("could not get schema with id: %s", *id) + sr.logger.Printf(errors.Wrap(err, errMsg).Error()) + return framework.NewRequestErrorMsg(errMsg, http.StatusBadRequest) + } + + resp := GetSchemaResponse{Schema: gotSchema.Schema} + return framework.Respond(ctx, w, resp, http.StatusOK) +} diff --git a/pkg/server/router/schema_test.go b/pkg/server/router/schema_test.go new file mode 100644 index 000000000..d3aabd55a --- /dev/null +++ b/pkg/server/router/schema_test.go @@ -0,0 +1,107 @@ +package router + +import ( + "github.com/stretchr/testify/assert" + "github.com/tbd54566975/ssi-service/pkg/service/framework" + "github.com/tbd54566975/ssi-service/pkg/service/schema" + "github.com/tbd54566975/ssi-service/pkg/storage" + "log" + "os" + "testing" +) + +func TestSchemaRouter(t *testing.T) { + + // remove the db file after the test + t.Cleanup(func() { + _ = os.Remove(storage.DBFile) + }) + + t.Run("Nil Service", func(tt *testing.T) { + schemaRouter, err := NewSchemaRouter(nil, nil) + assert.Error(tt, err) + assert.Empty(tt, schemaRouter) + assert.Contains(tt, err.Error(), "service cannot be nil") + }) + + t.Run("Bad Service", func(tt *testing.T) { + schemaRouter, err := NewSchemaRouter(&testService{}, nil) + assert.Error(tt, err) + assert.Empty(tt, schemaRouter) + assert.Contains(tt, err.Error(), "could not create schema router with service type: test") + }) + + t.Run("Schema Service Test", func(tt *testing.T) { + logger := log.New(os.Stdout, "ssi-test", log.LstdFlags) + + bolt, err := storage.NewBoltDB(logger) + assert.NoError(tt, err) + assert.NotEmpty(tt, bolt) + + schemaService, err := schema.NewSchemaService(logger, bolt) + assert.NoError(tt, err) + assert.NotEmpty(tt, schemaService) + + // check type and status + assert.Equal(tt, framework.Schema, schemaService.Type()) + assert.Equal(tt, framework.StatusReady, schemaService.Status().Status) + + // get all schemas (none) + gotSchemas, err := schemaService.GetSchemas() + assert.NoError(tt, err) + assert.Empty(tt, gotSchemas) + assert.Equal(tt, 0, len(gotSchemas.Schemas)) + + // get schema that doesn't exist + _, err = schemaService.GetSchemaByID(schema.GetSchemaByIDRequest{ID: "bad"}) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "error getting schema") + + // create a schema + simpleSchema := map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "foo": map[string]interface{}{ + "type": "string", + }, + }, + "required": []interface{}{"foo"}, + "additionalProperties": false, + } + createdSchema, err := schemaService.CreateSchema(schema.CreateSchemaRequest{Author: "me", Name: "simple schema", Schema: simpleSchema}) + assert.NoError(tt, err) + assert.NotEmpty(tt, createdSchema) + assert.NotEmpty(tt, createdSchema.ID) + assert.Equal(tt, "me", createdSchema.Schema.Author) + assert.Equal(tt, "simple schema", createdSchema.Schema.Name) + + // get schema by ID + gotSchema, err := schemaService.GetSchemaByID(schema.GetSchemaByIDRequest{ID: createdSchema.ID}) + assert.NoError(tt, err) + assert.NotEmpty(tt, gotSchema) + assert.EqualValues(tt, createdSchema.Schema, gotSchema.Schema) + + // get all schemas, expect one + gotSchemas, err = schemaService.GetSchemas() + assert.NoError(tt, err) + assert.NotEmpty(tt, gotSchemas.Schemas) + assert.Len(tt, gotSchemas.Schemas, 1) + + // store another + createdSchema, err = schemaService.CreateSchema(schema.CreateSchemaRequest{Author: "me", Name: "simple schema 2", Schema: simpleSchema}) + assert.NoError(tt, err) + assert.NotEmpty(tt, createdSchema) + assert.NotEmpty(tt, createdSchema.ID) + assert.Equal(tt, "me", createdSchema.Schema.Author) + assert.Equal(tt, "simple schema 2", createdSchema.Schema.Name) + + // get all schemas, expect two + gotSchemas, err = schemaService.GetSchemas() + assert.NoError(tt, err) + assert.NotEmpty(tt, gotSchemas.Schemas) + assert.Len(tt, gotSchemas.Schemas, 2) + + // make sure their IDs are different + assert.True(tt, gotSchemas.Schemas[0].ID != gotSchemas.Schemas[1].ID) + }) +} diff --git a/pkg/server/server.go b/pkg/server/server.go index 4571bfb34..874cf2985 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -18,8 +18,9 @@ import ( ) const ( - V1Prefix = "/v1" - DIDsPrefix = "/dids" + V1Prefix = "/v1" + DIDsPrefix = "/dids" + SchemasPrefix = "/schemas" ) // SSIServer exposes all dependencies needed to run a http server and all its services @@ -74,6 +75,8 @@ func (s *SSIServer) instantiateRouter(service svcframework.Service) error { switch serviceType { case svcframework.DID: return s.DecentralizedIdentityAPI(service) + case svcframework.Schema: + return s.SchemaAPI(service) default: return fmt.Errorf("could not instantiate API for service: %s", serviceType) } @@ -93,3 +96,18 @@ func (s *SSIServer) DecentralizedIdentityAPI(service svcframework.Service) error s.Handle(http.MethodGet, path.Join(handlerPath, "/:method/:id"), didRouter.GetDIDByMethod) return nil } + +// SchemaAPI registers all HTTP router for the Schema Service +func (s *SSIServer) SchemaAPI(service svcframework.Service) error { + schemaRouter, err := router.NewSchemaRouter(service, s.Logger) + if err != nil { + return errors.Wrap(err, "could not create Schema router") + } + + handlerPath := V1Prefix + SchemasPrefix + + s.Handle(http.MethodPut, handlerPath, schemaRouter.CreateSchema) + s.Handle(http.MethodGet, handlerPath, schemaRouter.GetSchemas) + s.Handle(http.MethodGet, path.Join(handlerPath, "/:id"), schemaRouter.GetSchemaByID) + return nil +} diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go index af9e9fbda..7c97f5b16 100644 --- a/pkg/server/server_test.go +++ b/pkg/server/server_test.go @@ -15,6 +15,7 @@ import ( "github.com/tbd54566975/ssi-service/pkg/service" "github.com/tbd54566975/ssi-service/pkg/service/did" svcframework "github.com/tbd54566975/ssi-service/pkg/service/framework" + "github.com/tbd54566975/ssi-service/pkg/service/schema" "github.com/tbd54566975/ssi-service/pkg/storage" "io" "log" @@ -25,66 +26,68 @@ import ( "time" ) -func TestAPI(t *testing.T) { - +func TestHealthCheckAPI(t *testing.T) { // remove the db file after the test t.Cleanup(func() { _ = os.Remove(storage.DBFile) }) - t.Run("Test Health Check", func(tt *testing.T) { - shutdown := make(chan os.Signal, 1) - logger := log.New(os.Stdout, "ssi-test", log.LstdFlags) - server, err := NewSSIServer(shutdown, service.Config{Logger: logger}) - assert.NoError(tt, err) - assert.NotEmpty(tt, server) + shutdown := make(chan os.Signal, 1) + logger := log.New(os.Stdout, "ssi-test", log.LstdFlags) + server, err := NewSSIServer(shutdown, service.Config{Logger: logger}) + assert.NoError(t, err) + assert.NotEmpty(t, server) - req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/health", nil) - w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/health", nil) + w := httptest.NewRecorder() - err = router.Health(context.TODO(), w, req) - assert.NoError(tt, err) - assert.Equal(tt, http.StatusOK, w.Result().StatusCode) + err = router.Health(context.TODO(), w, req) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, w.Result().StatusCode) - var resp router.GetHealthCheckResponse - err = json.NewDecoder(w.Body).Decode(&resp) - assert.NoError(tt, err) + var resp router.GetHealthCheckResponse + err = json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(t, err) + + assert.Equal(t, router.HealthOK, resp.Status) - assert.Equal(tt, router.HealthOK, resp.Status) +} + +func TestReadinessAPI(t *testing.T) { + // remove the db file after the test + t.Cleanup(func() { + _ = os.Remove(storage.DBFile) }) - t.Run("Test Readiness Check", func(tt *testing.T) { - shutdown := make(chan os.Signal, 1) - logger := log.New(os.Stdout, "ssi-test", log.LstdFlags) - server, err := NewSSIServer(shutdown, service.Config{Logger: logger}) - assert.NoError(tt, err) - assert.NotEmpty(tt, server) + shutdown := make(chan os.Signal, 1) + logger := log.New(os.Stdout, "ssi-test", log.LstdFlags) + server, err := NewSSIServer(shutdown, service.Config{Logger: logger}) + assert.NoError(t, err) + assert.NotEmpty(t, server) - req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/readiness", nil) - w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/readiness", nil) + w := httptest.NewRecorder() - handler := router.Readiness(nil, logger) - err = handler(newRequestContext(), w, req) - assert.NoError(tt, err) - assert.Equal(tt, http.StatusOK, w.Result().StatusCode) + handler := router.Readiness(nil, logger) + err = handler(newRequestContext(), w, req) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, w.Result().StatusCode) - var resp router.GetReadinessResponse - err = json.NewDecoder(w.Body).Decode(&resp) - assert.NoError(tt, err) + var resp router.GetReadinessResponse + err = json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(t, err) - assert.Equal(tt, svcframework.StatusReady, resp.Status.Status) - assert.Len(tt, resp.ServiceStatuses, 0) - }) + assert.Equal(t, svcframework.StatusReady, resp.Status.Status) + assert.Len(t, resp.ServiceStatuses, 0) } func TestDIDAPI(t *testing.T) { - - // remove the db file after the test - t.Cleanup(func() { - _ = os.Remove(storage.DBFile) - }) - t.Run("Test Get DID Methods", func(tt *testing.T) { + // remove the db file after the test + tt.Cleanup(func() { + _ = os.Remove(storage.DBFile) + }) + didRouter := newDIDService(tt) // get DID methods @@ -104,6 +107,11 @@ func TestDIDAPI(t *testing.T) { }) t.Run("Test Create DID By Method: Key", func(tt *testing.T) { + // remove the db file after the test + tt.Cleanup(func() { + _ = os.Remove(storage.DBFile) + }) + didRouter := newDIDService(tt) // create DID by method - key - missing body @@ -118,7 +126,7 @@ func TestDIDAPI(t *testing.T) { assert.Contains(tt, err.Error(), "invalid create DID request") // with body, bad key type - createDIDRequest := did.CreateDIDRequest{KeyType: "bad"} + createDIDRequest := router.CreateDIDByMethodRequest{KeyType: "bad"} requestReader := newRequestValue(tt, createDIDRequest) req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/key", requestReader) w = httptest.NewRecorder() @@ -128,7 +136,7 @@ func TestDIDAPI(t *testing.T) { assert.Contains(tt, err.Error(), "could not create DID for method with key type: bad") // with body, good key type - createDIDRequest = did.CreateDIDRequest{KeyType: crypto.Ed25519} + createDIDRequest = router.CreateDIDByMethodRequest{KeyType: crypto.Ed25519} requestReader = newRequestValue(tt, createDIDRequest) req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/key", requestReader) w = httptest.NewRecorder() @@ -144,6 +152,11 @@ func TestDIDAPI(t *testing.T) { }) t.Run("Test Get DID By Method", func(tt *testing.T) { + // remove the db file after the test + tt.Cleanup(func() { + _ = os.Remove(storage.DBFile) + }) + didRouter := newDIDService(tt) // get DID by method @@ -157,7 +170,7 @@ func TestDIDAPI(t *testing.T) { } err := didRouter.GetDIDByMethod(newRequestContextWithParams(badParams), w, req) assert.Error(tt, err) - assert.Contains(tt, err.Error(), "could not get handler for method") + assert.Contains(tt, err.Error(), "could not get DID for method") // good method, bad id badParams1 := map[string]string{ @@ -169,7 +182,7 @@ func TestDIDAPI(t *testing.T) { assert.Contains(tt, err.Error(), "could not get DID for method with id: worse") // store a DID - createDIDRequest := did.CreateDIDRequest{KeyType: crypto.Ed25519} + createDIDRequest := router.CreateDIDByMethodRequest{KeyType: crypto.Ed25519} requestReader := newRequestValue(tt, createDIDRequest) params := map[string]string{"method": "key"} req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/key", requestReader) @@ -205,11 +218,12 @@ func TestDIDAPI(t *testing.T) { func newDIDService(t *testing.T) *router.DIDRouter { // set up DID service - bolt, err := storage.NewBoltDB() + logger := log.New(os.Stdout, "ssi-test", log.LstdFlags) + + bolt, err := storage.NewBoltDB(logger) require.NoError(t, err) require.NotEmpty(t, bolt) - logger := log.New(os.Stdout, "ssi-test", log.LstdFlags) didService, err := did.NewDIDService(logger, []did.Method{did.KeyMethod}, bolt) require.NoError(t, err) require.NotEmpty(t, didService) @@ -222,6 +236,163 @@ func newDIDService(t *testing.T) *router.DIDRouter { return didRouter } +func TestSchemaAPI(t *testing.T) { + t.Run("Test Create Schema", func(tt *testing.T) { + // remove the db file after the test + tt.Cleanup(func() { + _ = os.Remove(storage.DBFile) + }) + + schemaRouter := newSchemaService(tt) + + simpleSchema := map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "foo": map[string]interface{}{ + "type": "string", + }, + }, + "required": []interface{}{"foo"}, + "additionalProperties": false, + } + badSchemaRequest := router.CreateSchemaRequest{Schema: simpleSchema} + schemaRequestValue := newRequestValue(tt, badSchemaRequest) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/schemas", schemaRequestValue) + w := httptest.NewRecorder() + + err := schemaRouter.CreateSchema(newRequestContext(), w, req) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "invalid create schema request") + + // reset the http recorder + w.Flush() + + schemaRequest := router.CreateSchemaRequest{Author: "did:test", Name: "test schema", Schema: simpleSchema} + schemaRequestValue = newRequestValue(tt, schemaRequest) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/schemas", schemaRequestValue) + err = schemaRouter.CreateSchema(newRequestContext(), w, req) + assert.NoError(tt, err) + + var resp router.CreateSchemaResponse + err = json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(tt, err) + + assert.NotEmpty(tt, resp.ID) + assert.EqualValues(tt, schemaRequest.Schema, resp.Schema.Schema) + }) + + t.Run("Test Get Schema/s", func(tt *testing.T) { + // remove the db file after the test + tt.Cleanup(func() { + _ = os.Remove(storage.DBFile) + }) + + schemaRouter := newSchemaService(tt) + + // get schema that doesn't exist + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/schemas/bad", nil) + err := schemaRouter.GetSchemaByID(newRequestContext(), w, req) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "cannot get schema without ID parameter") + + // reset recorder between calls + w.Flush() + + // get schema with invalid id + req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/schemas/bad", nil) + err = schemaRouter.GetSchemaByID(newRequestContextWithParams(map[string]string{"id": "bad"}), w, req) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "could not get schema with id: bad") + + // reset recorder between calls + w.Flush() + + // get all schemas - get none + req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/schemas", nil) + err = schemaRouter.GetSchemas(newRequestContext(), w, req) + assert.NoError(tt, err) + var getSchemasResp router.GetSchemasResponse + err = json.NewDecoder(w.Body).Decode(&getSchemasResp) + assert.NoError(tt, err) + assert.Len(tt, getSchemasResp.Schemas, 0) + + // reset recorder between calls + w.Flush() + + // create a schema + simpleSchema := map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "foo": map[string]interface{}{ + "type": "string", + }, + }, + "required": []interface{}{"foo"}, + "additionalProperties": false, + } + schemaRequest := router.CreateSchemaRequest{Author: "did:test", Name: "test schema", Schema: simpleSchema} + schemaRequestValue := newRequestValue(tt, schemaRequest) + createReq := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/schemas", schemaRequestValue) + + err = schemaRouter.CreateSchema(newRequestContext(), w, createReq) + assert.NoError(tt, err) + + var createResp router.CreateSchemaResponse + err = json.NewDecoder(w.Body).Decode(&createResp) + assert.NoError(tt, err) + + assert.NotEmpty(tt, createResp.ID) + assert.EqualValues(tt, schemaRequest.Schema, createResp.Schema.Schema) + + // reset recorder between calls + w.Flush() + + // get it back + req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/schemas/%s", createResp.ID), nil) + err = schemaRouter.GetSchemaByID(newRequestContextWithParams(map[string]string{"id": createResp.ID}), w, req) + assert.NoError(tt, err) + + var gotSchemaResp router.GetSchemaResponse + err = json.NewDecoder(w.Body).Decode(&gotSchemaResp) + assert.NoError(tt, err) + + assert.Equal(tt, createResp.ID, gotSchemaResp.Schema.ID) + assert.Equal(tt, createResp.Schema.Schema, gotSchemaResp.Schema.Schema) + + // reset recorder between calls + w.Flush() + + // get all schemas - get none + req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/schemas", nil) + err = schemaRouter.GetSchemas(newRequestContext(), w, req) + assert.NoError(tt, err) + err = json.NewDecoder(w.Body).Decode(&getSchemasResp) + assert.NoError(tt, err) + assert.Len(tt, getSchemasResp.Schemas, 1) + }) +} + +func newSchemaService(t *testing.T) *router.SchemaRouter { + // set up DID service + logger := log.New(os.Stdout, "ssi-test", log.LstdFlags) + + bolt, err := storage.NewBoltDB(logger) + require.NoError(t, err) + require.NotEmpty(t, bolt) + + schemaService, err := schema.NewSchemaService(logger, bolt) + require.NoError(t, err) + require.NotEmpty(t, schemaService) + + // create router for service + schemaRouter, err := router.NewSchemaRouter(schemaService, logger) + require.NoError(t, err) + require.NotEmpty(t, schemaRouter) + + return schemaRouter +} + func newRequestValue(t *testing.T, data interface{}) io.Reader { dataBytes, err := json.Marshal(data) require.NoError(t, err) diff --git a/pkg/service/did/did.go b/pkg/service/did/did.go index bfd70b7b9..f0edfcd3f 100644 --- a/pkg/service/did/did.go +++ b/pkg/service/did/did.go @@ -19,7 +19,7 @@ type Service struct { // supported DID methods handlers map[Method]MethodHandler storage didstorage.Storage - log *log.Logger + logger *log.Logger } func (s Service) Type() framework.Type { @@ -37,15 +37,35 @@ func (s Service) Status() framework.Status { return framework.Status{Status: framework.StatusReady} } -func (s Service) GetSupportedMethods() []Method { +func (s Service) GetSupportedMethods() GetSupportedMethodsResponse { var methods []Method for method := range s.handlers { methods = append(methods, method) } - return methods + return GetSupportedMethodsResponse{Methods: methods} } -func (s Service) GetHandler(method Method) (MethodHandler, error) { +func (s Service) CreateDIDByMethod(request CreateDIDRequest) (*CreateDIDResponse, error) { + handler, err := s.getHandler(request.Method) + if err != nil { + errMsg := fmt.Sprintf("could not get handler for method<%s>", request.Method) + s.logger.Printf(errMsg) + return nil, errors.New(errMsg) + } + return handler.CreateDID(request) +} + +func (s Service) GetDIDByMethod(request GetDIDRequest) (*GetDIDResponse, error) { + handler, err := s.getHandler(request.Method) + if err != nil { + errMsg := fmt.Sprintf("could not get handler for method<%s>", request.Method) + s.logger.Printf(errMsg) + return nil, errors.New(errMsg) + } + return handler.GetDID(request) +} + +func (s Service) getHandler(method Method) (MethodHandler, error) { handler, ok := s.handlers[method] if !ok { return nil, fmt.Errorf("could not get handler for DID method: %s", method) @@ -56,18 +76,18 @@ func (s Service) GetHandler(method Method) (MethodHandler, error) { // MethodHandler describes the functionality of *all* possible DID service, regardless of method type MethodHandler interface { CreateDID(request CreateDIDRequest) (*CreateDIDResponse, error) - GetDID(id string) (*GetDIDResponse, error) + GetDID(request GetDIDRequest) (*GetDIDResponse, error) } func NewDIDService(log *log.Logger, methods []Method, s storage.ServiceStorage) (*Service, error) { didStorage, err := didstorage.NewDIDStorage(s) if err != nil { - return nil, errors.Wrap(err, "could not instantiate DID storage for DID service") + return nil, errors.Wrap(err, "could not instantiate DID storage for the DID service") } svc := Service{ storage: didStorage, handlers: make(map[Method]MethodHandler), - log: log, + logger: log, } // instantiate all handlers for DID methods diff --git a/pkg/service/did/key.go b/pkg/service/did/key.go index ff90e916f..63971459b 100644 --- a/pkg/service/did/key.go +++ b/pkg/service/did/key.go @@ -49,17 +49,16 @@ func (h *keyDIDHandler) CreateDID(request CreateDIDRequest) (*CreateDIDResponse, }, nil } -func (h *keyDIDHandler) GetDID(id string) (*GetDIDResponse, error) { +func (h *keyDIDHandler) GetDID(request GetDIDRequest) (*GetDIDResponse, error) { + id := request.ID gotDID, err := h.storage.GetDID(id) if err != nil { return nil, fmt.Errorf("error getting DID: %s", id) } if gotDID == nil { - return nil, fmt.Errorf("DID with id<%s> could not be found", id) + return nil, fmt.Errorf("did with id<%s> could not be found", id) } - return &GetDIDResponse{ - DID: gotDID.DID, - }, nil + return &GetDIDResponse{DID: gotDID.DID}, nil } func privateKeyToBase58(privKey interface{}) (string, error) { diff --git a/pkg/service/did/models.go b/pkg/service/did/model.go similarity index 65% rename from pkg/service/did/models.go rename to pkg/service/did/model.go index f69599460..763397697 100644 --- a/pkg/service/did/models.go +++ b/pkg/service/did/model.go @@ -5,8 +5,13 @@ import ( sdkdid "github.com/TBD54566975/ssi-sdk/did" ) -// CreateDIDRequest is the SON-serializable request for creating a DID across DID methods +type GetSupportedMethodsResponse struct { + Methods []Method `json:"methods"` +} + +// CreateDIDRequest is the JSON-serializable request for creating a DID across DID methods type CreateDIDRequest struct { + Method Method `json:"method" validate:"required"` KeyType crypto.KeyType `validate:"required"` } @@ -17,6 +22,11 @@ type CreateDIDResponse struct { PrivateKey string `json:"base58PrivateKey"` } +type GetDIDRequest struct { + Method Method `json:"method" validate:"required"` + ID string `json:"id" validate:"required"` +} + // GetDIDResponse is the JSON-serializable response for getting a DID type GetDIDResponse struct { DID sdkdid.DIDDocument `json:"did"` diff --git a/pkg/service/did/storage/bolt.go b/pkg/service/did/storage/bolt.go index 9b43cbbf7..d86ba5f8e 100644 --- a/pkg/service/did/storage/bolt.go +++ b/pkg/service/did/storage/bolt.go @@ -4,8 +4,8 @@ import ( "encoding/json" "fmt" "github.com/pkg/errors" + "github.com/tbd54566975/ssi-service/internal/util" "github.com/tbd54566975/ssi-service/pkg/storage" - "strings" ) const ( @@ -54,7 +54,7 @@ func (b BoltDIDStorage) GetDID(id string) (*StoredDID, error) { return nil, errors.Wrap(err, couldNotGetDIDErr) } if len(docBytes) == 0 { - return nil, fmt.Errorf("DID not found: %s", id) + return nil, fmt.Errorf("did not found: %s", id) } var stored StoredDID if err := json.Unmarshal(docBytes, &stored); err != nil { @@ -63,7 +63,7 @@ func (b BoltDIDStorage) GetDID(id string) (*StoredDID, error) { return &stored, nil } -// GetDIDs attempts to get all DIDs for a given method. It will return those it can, even if it has trouble with some. +// GetDIDs attempts to get all DIDs for a given method. It will return those it can even if it has trouble with some. func (b BoltDIDStorage) GetDIDs(method string) ([]StoredDID, error) { couldNotGetDIDsErr := fmt.Sprintf("could not get DIDs for method: %s", method) namespace, err := getNamespaceForMethod(method) @@ -93,11 +93,14 @@ func (b BoltDIDStorage) DeleteDID(id string) error { if err != nil { return errors.Wrap(err, couldNotGetDIDErr) } - return b.db.Delete(namespace, id) + if err := b.db.Delete(namespace, id); err != nil { + return errors.Wrapf(err, "could not delete DID: %s", id) + } + return nil } func getNamespaceForDID(id string) (string, error) { - method, err := getMethod(id) + method, err := util.GetMethodForDID(id) if err != nil { return "", err } @@ -108,15 +111,6 @@ func getNamespaceForDID(id string) (string, error) { return namespace, nil } -// getMethod gets a DID method from a did, the second part of the did (e.g. did:test:abcd, the method is 'test') -func getMethod(did string) (string, error) { - split := strings.Split(did, ":") - if len(split) < 3 { - return "", fmt.Errorf("malformed did: %s", did) - } - return split[1], nil -} - func getNamespaceForMethod(method string) (string, error) { namespace, ok := didMethodToNamespace[method] if !ok { diff --git a/pkg/service/did/storage/storage.go b/pkg/service/did/storage/storage.go index 2de509b0e..43492489a 100644 --- a/pkg/service/did/storage/storage.go +++ b/pkg/service/did/storage/storage.go @@ -7,8 +7,8 @@ import ( ) type StoredDID struct { - DID did.DIDDocument - PrivateKeyBase58 string + DID did.DIDDocument `json:"did"` + PrivateKeyBase58 string `json:"privateKeyBase58"` } type Storage interface { diff --git a/pkg/service/framework/framework.go b/pkg/service/framework/framework.go index cd6a93125..5e1b68a0d 100644 --- a/pkg/service/framework/framework.go +++ b/pkg/service/framework/framework.go @@ -8,10 +8,11 @@ type ( const ( // List of all service - DID Type = "did-service" + DID Type = "did-service" + Schema Type = "schema-service" StatusReady StatusState = "ready" - StatusNotReady StatusState = "not ready" + StatusNotReady StatusState = "not_ready" ) // Status is for service reporting on their status diff --git a/pkg/service/schema/model.go b/pkg/service/schema/model.go new file mode 100644 index 000000000..c083a5061 --- /dev/null +++ b/pkg/service/schema/model.go @@ -0,0 +1,30 @@ +package schema + +import "github.com/TBD54566975/ssi-sdk/credential/schema" + +const ( + Version1 string = "1.0.0" +) + +type GetSchemasResponse struct { + Schemas []schema.VCJSONSchema `json:"schemas,omitempty"` +} + +type CreateSchemaRequest struct { + Author string `json:"author" validate:"required"` + Name string `json:"name" validate:"required"` + Schema schema.JSONSchema `json:"schema" validate:"required"` +} + +type CreateSchemaResponse struct { + ID string `json:"id"` + Schema schema.VCJSONSchema `json:"schema"` +} + +type GetSchemaByIDRequest struct { + ID string `json:"id" validate:"required"` +} + +type GetSchemaByIDResponse struct { + Schema schema.VCJSONSchema `json:"schema"` +} diff --git a/pkg/service/schema/schema.go b/pkg/service/schema/schema.go new file mode 100644 index 000000000..c35a81d93 --- /dev/null +++ b/pkg/service/schema/schema.go @@ -0,0 +1,101 @@ +package schema + +import ( + "encoding/json" + "fmt" + "github.com/TBD54566975/ssi-sdk/credential/schema" + schemalib "github.com/TBD54566975/ssi-sdk/schema" + "github.com/google/uuid" + "github.com/pkg/errors" + "github.com/tbd54566975/ssi-service/pkg/service/framework" + schemastorage "github.com/tbd54566975/ssi-service/pkg/service/schema/storage" + "github.com/tbd54566975/ssi-service/pkg/storage" + "log" + "time" +) + +type Service struct { + storage schemastorage.Storage + log *log.Logger +} + +func (s Service) Type() framework.Type { + return framework.Schema +} + +func (s Service) Status() framework.Status { + if s.storage == nil { + return framework.Status{ + Status: framework.StatusNotReady, + Message: "storage not loaded", + } + } + return framework.Status{Status: framework.StatusReady} +} + +func NewSchemaService(logger *log.Logger, s storage.ServiceStorage) (*Service, error) { + schemaStorage, err := schemastorage.NewSchemaStorage(s) + if err != nil { + return nil, errors.Wrap(err, "could not instantiate Schema storage for the Schema service") + } + return &Service{ + storage: schemaStorage, + log: logger, + }, nil +} + +// CreateSchema houses the main service logic for schema creation. It validates the input, and +// produces a schema value that conforms with the VC JSON Schema specification. +// TODO(gabe) support proof generation on schemas, versioning, and more +func (s Service) CreateSchema(request CreateSchemaRequest) (*CreateSchemaResponse, error) { + schemaBytes, err := json.Marshal(request.Schema) + if err != nil { + return nil, errors.Wrap(err, "could not marshal schema in request") + } + if err := schemalib.IsValidJSONSchema(string(schemaBytes)); err != nil { + return nil, errors.Wrap(err, "provided value is not a valid JSON schema") + } + + schemaID := uuid.NewString() + schema := schema.VCJSONSchema{ + Type: schema.VCJSONSchemaType, + Version: Version1, + ID: schemaID, + Name: request.Name, + Author: request.Author, + Authored: time.Now().Format(time.RFC3339), + Schema: request.Schema, + } + + storedSchema := schemastorage.StoredSchema{Schema: schema} + if err := s.storage.StoreSchema(storedSchema); err != nil { + return nil, errors.Wrap(err, "could not store schema") + } + + return &CreateSchemaResponse{ID: schemaID, Schema: schema}, nil +} + +func (s Service) GetSchemas() (*GetSchemasResponse, error) { + storedSchemas, err := s.storage.GetSchemas() + if err != nil { + return nil, errors.Wrap(err, "error getting schemas") + } + var schemas []schema.VCJSONSchema + for _, stored := range storedSchemas { + schemas = append(schemas, stored.Schema) + } + return &GetSchemasResponse{ + Schemas: schemas, + }, nil +} + +func (s Service) GetSchemaByID(request GetSchemaByIDRequest) (*GetSchemaByIDResponse, error) { + gotSchema, err := s.storage.GetSchema(request.ID) + if err != nil { + return nil, fmt.Errorf("error getting schema: %s", request.ID) + } + if gotSchema == nil { + return nil, fmt.Errorf("schema with id<%s> could not be found", request.ID) + } + return &GetSchemaByIDResponse{Schema: gotSchema.Schema}, nil +} diff --git a/pkg/service/schema/storage/bolt.go b/pkg/service/schema/storage/bolt.go new file mode 100644 index 000000000..daacfad29 --- /dev/null +++ b/pkg/service/schema/storage/bolt.go @@ -0,0 +1,76 @@ +package storage + +import ( + "encoding/json" + "fmt" + "github.com/pkg/errors" + "github.com/tbd54566975/ssi-service/pkg/storage" +) + +const ( + namespace = "schema" +) + +type BoltSchemaStorage struct { + db *storage.BoltDB +} + +func NewBoltSchemaStorage(db *storage.BoltDB) (*BoltSchemaStorage, error) { + if db == nil { + return nil, errors.New("bolt db reference is nil") + } + return &BoltSchemaStorage{db: db}, nil +} + +func (b BoltSchemaStorage) StoreSchema(schema StoredSchema) error { + id := schema.Schema.ID + if id == "" { + return errors.New("could not store schema without an ID") + } + schemaBytes, err := json.Marshal(schema) + if err != nil { + return errors.Wrapf(err, "could not store Schema: %s", id) + } + return b.db.Write(namespace, id, schemaBytes) +} + +func (b BoltSchemaStorage) GetSchema(id string) (*StoredSchema, error) { + schemaBytes, err := b.db.Read(namespace, id) + if err != nil { + return nil, errors.Wrapf(err, "could not get schema: %s", id) + } + if len(schemaBytes) == 0 { + return nil, fmt.Errorf("schema not found with id: %s", id) + } + var stored StoredSchema + if err := json.Unmarshal(schemaBytes, &stored); err != nil { + return nil, errors.Wrapf(err, "could not unmarshal stored schema: %s", id) + } + return &stored, nil +} + +// GetSchemas attempts to get all stored schemas. It will return those it can even if it has trouble with some. +func (b BoltSchemaStorage) GetSchemas() ([]StoredSchema, error) { + gotSchemas, err := b.db.ReadAll(namespace) + if err != nil { + return nil, errors.Wrap(err, "could not get all schemas") + } + if len(gotSchemas) == 0 { + return nil, nil + } + var stored []StoredSchema + for _, schemaBytes := range gotSchemas { + var nextSchema StoredSchema + if err := json.Unmarshal(schemaBytes, &nextSchema); err == nil { + stored = append(stored, nextSchema) + } + } + return stored, nil +} + +func (b BoltSchemaStorage) DeleteSchema(id string) error { + if err := b.db.Delete(namespace, id); err != nil { + return errors.Wrapf(err, "could not delete schema: %s", id) + } + return nil +} diff --git a/pkg/service/schema/storage/storage.go b/pkg/service/schema/storage/storage.go new file mode 100644 index 000000000..db58776d8 --- /dev/null +++ b/pkg/service/schema/storage/storage.go @@ -0,0 +1,32 @@ +package storage + +import ( + "github.com/TBD54566975/ssi-sdk/credential/schema" + "github.com/pkg/errors" + "github.com/tbd54566975/ssi-service/pkg/storage" +) + +type StoredSchema struct { + Schema schema.VCJSONSchema `json:"schema"` +} + +type Storage interface { + StoreSchema(schema StoredSchema) error + GetSchema(id string) (*StoredSchema, error) + // TODO(gabe) consider get schemas by DID, or more advanced querying + GetSchemas() ([]StoredSchema, error) + DeleteSchema(id string) error +} + +// NewSchemaStorage finds the schema storage impl for a given ServiceStorage value +func NewSchemaStorage(s storage.ServiceStorage) (Storage, error) { + gotBolt, ok := s.(*storage.BoltDB) + if !ok { + return nil, errors.New("unsupported storage type") + } + boltStorage, err := NewBoltSchemaStorage(gotBolt) + if err != nil { + return nil, errors.Wrap(err, "could not instantiate Schema Bolt storage") + } + return boltStorage, err +} diff --git a/pkg/service/service.go b/pkg/service/service.go index d2cbfa76e..44e8efcbf 100644 --- a/pkg/service/service.go +++ b/pkg/service/service.go @@ -4,6 +4,7 @@ import ( "github.com/pkg/errors" "github.com/tbd54566975/ssi-service/pkg/service/did" "github.com/tbd54566975/ssi-service/pkg/service/framework" + "github.com/tbd54566975/ssi-service/pkg/service/schema" "github.com/tbd54566975/ssi-service/pkg/storage" "log" ) @@ -41,14 +42,18 @@ func (ssi *SSIService) GetServices() []framework.Service { } // instantiateServices begins all instantiates and their dependencies -func instantiateServices(log *log.Logger) ([]framework.Service, error) { - bolt, err := storage.NewBoltDB() +func instantiateServices(logger *log.Logger) ([]framework.Service, error) { + bolt, err := storage.NewBoltDB(logger) if err != nil { return nil, errors.Wrap(err, "could not instantiate BoltDB") } - didService, err := did.NewDIDService(log, []did.Method{did.KeyMethod}, bolt) + didService, err := did.NewDIDService(logger, []did.Method{did.KeyMethod}, bolt) if err != nil { return nil, errors.Wrap(err, "could not instantiate the DID service") } - return []framework.Service{didService}, nil + schemaService, err := schema.NewSchemaService(logger, bolt) + if err != nil { + return nil, errors.Wrap(err, "could not instantiate the schema service") + } + return []framework.Service{didService, schemaService}, nil } diff --git a/pkg/storage/bolt.go b/pkg/storage/bolt.go index c2543ec2e..8bad76972 100644 --- a/pkg/storage/bolt.go +++ b/pkg/storage/bolt.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/boltdb/bolt" "github.com/pkg/errors" + "log" "strings" "time" ) @@ -13,20 +14,21 @@ const ( ) type BoltDB struct { - db *bolt.DB + logger *log.Logger + db *bolt.DB } // NewBoltDB instantiates a file-based storage instance for Bolt https://github.com/boltdb/bolt -func NewBoltDB() (*BoltDB, error) { - return NewBoltDBWithFile(DBFile) +func NewBoltDB(logger *log.Logger) (*BoltDB, error) { + return NewBoltDBWithFile(logger, DBFile) } -func NewBoltDBWithFile(filePath string) (*BoltDB, error) { - db, err := bolt.Open(filePath, 0600, &bolt.Options{Timeout: 1 * time.Second}) +func NewBoltDBWithFile(logger *log.Logger, filePath string) (*BoltDB, error) { + db, err := bolt.Open(filePath, 0600, &bolt.Options{Timeout: 3 * time.Second}) if err != nil { return nil, err } - return &BoltDB{db: db}, nil + return &BoltDB{logger: logger, db: db}, nil } func (b *BoltDB) Close() error { @@ -51,7 +53,8 @@ func (b *BoltDB) Read(namespace, key string) ([]byte, error) { err := b.db.View(func(tx *bolt.Tx) error { bucket := tx.Bucket([]byte(namespace)) if bucket == nil { - return fmt.Errorf("namespace<%s> does not exist", namespace) + b.logger.Printf("namespace<%s> does not exist", namespace) + return nil } result = bucket.Get([]byte(key)) return nil @@ -64,7 +67,8 @@ func (b *BoltDB) ReadAll(namespace string) (map[string][]byte, error) { err := b.db.View(func(tx *bolt.Tx) error { bucket := tx.Bucket([]byte(namespace)) if bucket == nil { - return fmt.Errorf("namespace<%s> does not exist", namespace) + b.logger.Printf("namespace<%s> does not exist", namespace) + return nil } cursor := bucket.Cursor() for k, v := cursor.First(); k != nil; k, v = cursor.Next() { diff --git a/pkg/storage/bolt_test.go b/pkg/storage/bolt_test.go index 500035c88..12ef51bad 100644 --- a/pkg/storage/bolt_test.go +++ b/pkg/storage/bolt_test.go @@ -3,12 +3,15 @@ package storage import ( "encoding/json" "github.com/stretchr/testify/assert" + "log" "os" "testing" ) func TestBoltDB(t *testing.T) { - db, err := NewBoltDBWithFile("test.db") + logger := log.New(os.Stdout, "ssi-test", log.LstdFlags) + + db, err := NewBoltDBWithFile(logger, "test.db") assert.NoError(t, err) assert.NotEmpty(t, db) @@ -38,9 +41,9 @@ func TestBoltDB(t *testing.T) { assert.EqualValues(t, players1, players1Result) // get a value from a namespace that doesn't exist - _, err = db.Read("bad", "worse") - assert.Error(t, err) - assert.Contains(t, err.Error(), "namespace does not exist") + res, err := db.Read("bad", "worse") + assert.NoError(t, err) + assert.Empty(t, res) // get a value that doesn't exist in the namespace noValue, err := db.Read(namespace, "Porsche") @@ -88,7 +91,7 @@ func TestBoltDB(t *testing.T) { err = db.DeleteNamespace(namespace) assert.NoError(t, err) - _, err = db.Read(namespace, team1) - assert.Error(t, err) - assert.Contains(t, err.Error(), "namespace does not exist") + res, err = db.Read(namespace, team1) + assert.NoError(t, err) + assert.Empty(t, res) }