Skip to content
This repository has been archived by the owner on Dec 12, 2024. It is now read-only.

Added did ion deactivation endpoint. #646

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
24 changes: 18 additions & 6 deletions doc/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2893,11 +2893,23 @@ paths:
delete:
consumes:
- application/json
description: |-
When this is called with the correct did method and id it will flip the softDelete flag to true for the db entry.
A user can still get the did if they know the DID ID, and the did keys will still exist, but this did will not show up in the ListDIDsByMethod call
This facilitates a clean SSI-Service Admin UI but not leave any hanging VCs with inaccessible hanging DIDs.
Soft deletes a DID by its method
description: "Soft deletes and deactivates (when applicable) a DID for which
SSI is the custodian. The DID must have\nbeen previously created by calling\tthe
\"Create DID Document\" endpoint. The effects of Deleting a DID depend on
it's DID Method.\n\nWhen this is called, it will flip the `softDelete` flag
to true for the db entry.\nA user can still get the did if they know the DID
ID, and the did keys will still exist, but this did will not show up in the
ListDIDsByMethod call\nThis facilitates a clean SSI-Service Admin UI but not
leave any hanging VCs with inaccessible hanging DIDs.\n\nFor a DID who's DID
Method is `ion`, deactivation is also performed. The effects of deactivating
a DID include:\n\n* The `didDocumentMetadata.deactivated` property will be
set to `true` after\ndoing DID resolution (e.g. by calling the `v1/dids/resolution/<did>`
endpoint).\n* All the DID Document properties will be removed, except for
the `id` and `@context`. In practical terms, this\nmeans that no counterparty
will be able to obtain verification material from this DID.\n* All keys stored
by SSI service that are related to this DID (i.e. update, recovery, verification)
will be revoked.\n\nPlease note that deactivation of an `ion` DID is an irreversible
operation. For more details, refer to the sidetree spec at https://identity.foundation/sidetree/spec/#deactivate"
parameters:
- description: Method
in: path
Expand All @@ -2924,7 +2936,7 @@ paths:
description: Internal server error
schema:
type: string
summary: Soft delete a DID
summary: Deletes a DID
tags:
- DecentralizedIdentifiers
get:
Expand Down
25 changes: 18 additions & 7 deletions pkg/server/router/did.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,6 @@ func (dr DIDRouter) UpdateDIDByMethod(c *gin.Context) {

resp := CreateDIDByMethodResponse{DID: updateIONDIDResponse.DID}
framework.Respond(c, resp, http.StatusOK)

}

func toUpdateIONDIDRequest(id string, request UpdateDIDByMethodRequest) (*did.UpdateIONDIDRequest, error) {
Expand Down Expand Up @@ -399,13 +398,25 @@ type ResolveDIDResponse struct {
DIDDocumentMetadata *resolution.DocumentMetadata `json:"didDocumentMetadata,omitempty"`
}

// SoftDeleteDIDByMethod godoc
// DeleteDIDByMethod godoc
//
// @Description When this is called with the correct did method and id it will flip the softDelete flag to true for the db entry.
// @Summary Deletes a DID
// @Description Soft deletes and deactivates (when applicable) a DID for which SSI is the custodian. The DID must have
// @Description been previously created by calling the "Create DID Document" endpoint. The effects of Deleting a DID depend on it's DID Method.
// @Description
// @Description When this is called, it will flip the `softDelete` flag to true for the db entry.
// @Description A user can still get the did if they know the DID ID, and the did keys will still exist, but this did will not show up in the ListDIDsByMethod call
// @Description This facilitates a clean SSI-Service Admin UI but not leave any hanging VCs with inaccessible hanging DIDs.
// @Summary Soft delete a DID
// @Description Soft deletes a DID by its method
// @Description
// @Description For a DID who's DID Method is `ion`, deactivation is also performed. The effects of deactivating a DID include:
// @Description
// @Description * The `didDocumentMetadata.deactivated` property will be set to `true` after
// @Description doing DID resolution (e.g. by calling the `v1/dids/resolution/<did>` endpoint).
// @Description * All the DID Document properties will be removed, except for the `id` and `@context`. In practical terms, this
// @Description means that no counterparty will be able to obtain verification material from this DID.
// @Description * All keys stored by SSI service that are related to this DID (i.e. update, recovery, verification) will be revoked.
// @Description
// @Description Please note that deactivation of an `ion` DID is an irreversible operation. For more details, refer to the sidetree spec at https://identity.foundation/sidetree/spec/#deactivate
// @Tags DecentralizedIdentifiers
// @Accept json
// @Produce json
Expand All @@ -415,7 +426,7 @@ type ResolveDIDResponse struct {
// @Failure 400 {string} string "Bad request"
// @Failure 500 {string} string "Internal server error"
// @Router /v1/dids/{method}/{id} [delete]
func (dr DIDRouter) SoftDeleteDIDByMethod(c *gin.Context) {
func (dr DIDRouter) DeleteDIDByMethod(c *gin.Context) {
method := framework.GetParam(c, MethodParam)
if method == nil {
errMsg := "soft delete DID by method request missing method parameter"
Expand All @@ -430,7 +441,7 @@ func (dr DIDRouter) SoftDeleteDIDByMethod(c *gin.Context) {
}

deleteDIDRequest := did.DeleteDIDRequest{Method: didsdk.Method(*method), ID: *id}
if err := dr.service.SoftDeleteDIDByMethod(c, deleteDIDRequest); err != nil {
if err := dr.service.DeleteDIDByMethod(c, deleteDIDRequest); err != nil {
errMsg := fmt.Sprintf("could not soft delete DID with id: %s", *id)
framework.LoggingRespondErrWithMsg(c, err, errMsg, http.StatusInternalServerError)
return
Expand Down
8 changes: 4 additions & 4 deletions pkg/server/router/did_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,10 +145,10 @@ func TestDIDRouter(t *testing.T) {
assert.Len(tt, knownDIDs, 0)

// delete dids
err = didService.SoftDeleteDIDByMethod(context.Background(), did.DeleteDIDRequest{Method: didsdk.KeyMethod, ID: createDIDResponse.DID.ID})
err = didService.DeleteDIDByMethod(context.Background(), did.DeleteDIDRequest{Method: didsdk.KeyMethod, ID: createDIDResponse.DID.ID})
assert.NoError(tt, err)

err = didService.SoftDeleteDIDByMethod(context.Background(), did.DeleteDIDRequest{Method: didsdk.KeyMethod, ID: createDIDResponse2.DID.ID})
err = didService.DeleteDIDByMethod(context.Background(), did.DeleteDIDRequest{Method: didsdk.KeyMethod, ID: createDIDResponse2.DID.ID})
assert.NoError(tt, err)

// get all DIDs back
Expand Down Expand Up @@ -248,10 +248,10 @@ func TestDIDRouter(t *testing.T) {
assert.Len(tt, knownDIDs, 0)

// delete dids
err = didService.SoftDeleteDIDByMethod(context.Background(), did.DeleteDIDRequest{Method: didsdk.WebMethod, ID: createDIDResponse.DID.ID})
err = didService.DeleteDIDByMethod(context.Background(), did.DeleteDIDRequest{Method: didsdk.WebMethod, ID: createDIDResponse.DID.ID})
assert.NoError(tt, err)

err = didService.SoftDeleteDIDByMethod(context.Background(), did.DeleteDIDRequest{Method: didsdk.WebMethod, ID: createDIDResponse2.DID.ID})
err = didService.DeleteDIDByMethod(context.Background(), did.DeleteDIDRequest{Method: didsdk.WebMethod, ID: createDIDResponse2.DID.ID})
assert.NoError(tt, err)

// get all DIDs back
Expand Down
2 changes: 1 addition & 1 deletion pkg/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ func DecentralizedIdentityAPI(rg *gin.RouterGroup, service *didsvc.Service, did
didAPI.PUT("/:method/batch", middleware.Webhook(webhookService, webhook.DID, webhook.BatchCreate), batchDIDRouter.BatchCreateDIDs)
didAPI.GET("/:method", didRouter.ListDIDsByMethod)
didAPI.GET("/:method/:id", didRouter.GetDIDByMethod)
didAPI.DELETE("/:method/:id", didRouter.SoftDeleteDIDByMethod)
didAPI.DELETE("/:method/:id", didRouter.DeleteDIDByMethod)
didAPI.GET(ResolverPrefix+"/:id", didRouter.ResolveDID)
return
}
Expand Down
70 changes: 67 additions & 3 deletions pkg/server/server_did_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,71 @@ func TestDIDAPI(t *testing.T) {
assert.Len(tt, updateDIDResponse.DID.KeyAgreement, 1+len(createDIDResponse.DID.KeyAgreement))
assert.Len(tt, updateDIDResponse.DID.CapabilityInvocation, 0+len(createDIDResponse.DID.CapabilityInvocation))
assert.Len(tt, updateDIDResponse.DID.CapabilityInvocation, 0+len(createDIDResponse.DID.CapabilityInvocation))
})

t.Run("Create, deactivate, and resolve", func(tt *testing.T) {
// setup
db := test.ServiceStorage(t)
require.NotEmpty(tt, db)

_, keyStoreService, keyStoreServiceFactory := testKeyStore(tt, db)
didService, _ := testDIDRouter(tt, db, keyStoreService, []string{"ion"}, keyStoreServiceFactory)

params := map[string]string{
"method": "ion",
}
w := httptest.NewRecorder()

gock.New(testIONResolverURL).
Post("/operations").
Reply(200).
JSON(string(BasicDIDResolution))
defer gock.Off()

// create the did
createDIDRequest := router.CreateDIDByMethodRequest{KeyType: crypto.Ed25519}
requestReader := newRequestValue(tt, createDIDRequest)
req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/ion", requestReader)

c := newRequestContextWithParams(w, req, params)
didService.CreateDIDByMethod(c)
assert.True(tt, util.Is2xxResponse(w.Code))

var createDIDResponse router.CreateDIDByMethodResponse
err := json.NewDecoder(w.Body).Decode(&createDIDResponse)
assert.NoError(tt, err)

// delete and deactivate it
w = httptest.NewRecorder()
params["id"] = createDIDResponse.DID.ID
requestReader = newRequestValue(tt, nil)
req = httptest.NewRequest(http.MethodDelete, "https://ssi-service.com/v1/dids/ion/"+createDIDResponse.DID.ID, requestReader)

gock.New(testIONResolverURL).
Post("/operations").
Reply(200).
JSON("{}")
defer gock.Off()

c = newRequestContextWithParams(w, req, params)
didService.DeleteDIDByMethod(c)
assert.True(tt, util.Is2xxResponse(w.Code))

// And resolve it
w = httptest.NewRecorder()
req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/resolver/"+createDIDResponse.DID.ID, nil)
c = newRequestContextWithParams(w, req, params)
didService.ResolveDID(c)
assert.True(tt, util.Is2xxResponse(w.Code))

var resolveDIDResponse router.ResolveDIDResponse
err = json.NewDecoder(w.Body).Decode(&resolveDIDResponse)
assert.NoError(tt, err)

// verify that the resolution came through correctly
assert.True(tt, resolveDIDResponse.DIDDocumentMetadata.Deactivated)
assert.False(tt, resolveDIDResponse.DIDDocumentMetadata.Method.Published)
assert.Equal(tt, createDIDResponse.DID.ID, resolveDIDResponse.DIDDocumentMetadata.CanonicalID)
})

t.Run("Test Create Duplicate DID:Webs", func(tt *testing.T) {
Expand Down Expand Up @@ -525,7 +589,7 @@ func TestDIDAPI(t *testing.T) {
}

c := newRequestContextWithParams(w, req, badParams)
didService.SoftDeleteDIDByMethod(c)
didService.DeleteDIDByMethod(c)
assert.Contains(tt, w.Body.String(), "could not soft delete DID")

// good method, bad id
Expand All @@ -535,7 +599,7 @@ func TestDIDAPI(t *testing.T) {
}
w = httptest.NewRecorder()
c = newRequestContextWithParams(w, req, badParams1)
didService.SoftDeleteDIDByMethod(c)
didService.DeleteDIDByMethod(c)
assert.Contains(tt, w.Body.String(), "could not soft delete DID with id: worse: error getting DID: worse")

// store a DID
Expand Down Expand Up @@ -591,7 +655,7 @@ func TestDIDAPI(t *testing.T) {

w = httptest.NewRecorder()
c = newRequestContextWithParams(w, req, goodParams)
didService.SoftDeleteDIDByMethod(c)
didService.DeleteDIDByMethod(c)
assert.True(tt, util.Is2xxResponse(w.Code))

// get it back
Expand Down
41 changes: 41 additions & 0 deletions pkg/service/did/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package did

import (
"context"
"fmt"
"time"

"github.com/TBD54566975/ssi-sdk/did/resolution"
"github.com/sirupsen/logrus"
)

func resolve(ctx context.Context, id string, storage *Storage) (*resolution.Result, error) {
gotDID, err := storage.GetDIDDefault(ctx, 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)
}

createdAt, err := time.Parse(time.RFC3339, gotDID.CreatedAt)
if err != nil {
logrus.WithError(err).Errorf("parsing created at")
}
updatedAt, err := time.Parse(time.RFC3339, gotDID.UpdatedAt)
if err != nil {
logrus.WithError(err).Errorf("parsing created at")
}

const XMLFormat = "2006-01-02T15:04:05Z"

return &resolution.Result{
Context: "https://w3id.org/did-resolution/v1",
Document: gotDID.DID,
DocumentMetadata: &resolution.DocumentMetadata{
Created: createdAt.Format(XMLFormat),
Updated: updatedAt.Format(XMLFormat),
Deactivated: gotDID.SoftDeleted,
},
}, nil
}
25 changes: 6 additions & 19 deletions pkg/service/did/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,11 @@ type MethodHandler interface {
// ListDeletedDIDs returns all soft-deleted DIDs.
ListDeletedDIDs(ctx context.Context) (*ListDIDsResponse, error)

// SoftDeleteDID marks the given DID as deleted. It is not removed from storage.
SoftDeleteDID(ctx context.Context, request DeleteDIDRequest) error
// DeleteDID marks DIDs as deleted, and should do a reasonable effort to Delete. For instance, a DID ION would be deactivated.
DeleteDID(ctx context.Context, request DeleteDIDRequest) (*DeleteDIDResponse, error)

// Resolve returns the resolution result of the given DID according to https://w3c-ccg.github.io/did-resolution/#did-resolution-result.
Resolve(ctx context.Context, did string) (*resolution.Result, error)
decentralgabe marked this conversation as resolved.
Show resolved Hide resolved
}

// NewHandlerResolver creates a new HandlerResolver from a map of MethodHandlers which are used to resolve DIDs
Expand Down Expand Up @@ -62,23 +65,7 @@ type handlerResolver struct {
}

func (h handlerResolver) Resolve(ctx context.Context, did string, _ ...resolution.Option) (*resolution.Result, error) {
method, err := resolution.GetMethodForDID(did)
if err != nil {
return nil, errors.Wrap(err, "getting method from DID")
}

if method != h.method {
return nil, errors.Errorf("invalid method %s for handler %s", method, h.method)
}

gotDIDResponse, err := h.handler.GetDID(ctx, GetDIDRequest{
Method: h.method,
ID: did,
})
if err != nil {
return nil, errors.Wrap(err, "getting DID from handler")
}
return &resolution.Result{Document: gotDIDResponse.DID}, nil
return h.handler.Resolve(ctx, did)
}

func (h handlerResolver) Methods() []didsdk.Method {
Expand Down
Loading