Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added did ion deactivation endpoint. #646

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
67 changes: 67 additions & 0 deletions doc/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1567,6 +1567,15 @@ definitions:
- '@context'
- linked_dids
type: object
pkg_server_router.DeactivateDIDRequest:
type: object
pkg_server_router.DeactivateDIDResponse:
properties:
resolutionResult:
allOf:
- $ref: '#/definitions/resolution.Result'
description: The resolution result received from the ION node.
type: object
pkg_server_router.GetApplicationResponse:
properties:
application:
Expand Down Expand Up @@ -2280,6 +2289,17 @@ definitions:
description: The `updateCommitment` property in https://identity.foundation/sidetree/spec/#did-resolver-output
type: string
type: object
resolution.Result:
properties:
'@context':
type: string
didDocument:
$ref: '#/definitions/did.Document'
didDocumentMetadata:
$ref: '#/definitions/resolution.DocumentMetadata'
didResolutionMetadata:
$ref: '#/definitions/resolution.Metadata'
type: object
schema.JSONSchema:
additionalProperties: {}
type: object
Expand Down Expand Up @@ -2927,6 +2947,53 @@ paths:
summary: Updates a DID document.
tags:
- DecentralizedIdentityAPI
/v1/dids/{method}/{id}/deactivation:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can be more restful as a delete to /v1/dids/{method}/{id}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have a that taken by SoftDelete

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm, I see. awkward because we both need to handle the service's concept of deletion and the did methods
I'm inclined to merge the two...
Something lke -- if the DID method supports deactivation, the DELETE to /v1/dids/{method}/{id} does both delete and deactivation. If the DID method does not support deactivation, we just do a delete.

Reason being, it's more confusing to support both separately. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that, will update.

put:
consumes:
- application/json
description: |-
Deactivates a DID for which SSI is the custodian. The DID must have been previously created by calling
the "Create DID Document" endpoint. Currently, only ION dids support deactivation. The
effect of deactivating a DID is that the `didDocumentMetadata.deactivated` will be set to `true` after
doing DID resolution (e.g. by calling the `v1/dids/resolution/<did>` endpoint). Additionally, all the
DID Document properties will be removed, except for the `id` and `@context`. In practical terms, this
means that no counterparty will be able to obtain verification material from this DID. Please not that
deactivation is an irreversible operation. For more details, refer to the sidetree spec at https://identity.foundation/sidetree/spec/#deactivate
parameters:
- description: Method
in: path
name: method
required: true
type: string
- description: ID
in: path
name: id
required: true
type: string
- description: request body
in: body
name: request
required: true
schema:
$ref: '#/definitions/pkg_server_router.DeactivateDIDRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/pkg_server_router.DeactivateDIDResponse'
"400":
description: Bad request
schema:
type: string
"500":
description: Internal server error
schema:
type: string
summary: Deactivate a DID document.
tags:
- DecentralizedIdentityAPI
/v1/dids/{method}/batch:
put:
consumes:
Expand Down
85 changes: 84 additions & 1 deletion 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 @@ -470,6 +469,90 @@ func (dr DIDRouter) ResolveDID(c *gin.Context) {
framework.Respond(c, resp, http.StatusOK)
}

type DeactivateDIDRequest struct{}

type DeactivateDIDResponse struct {
// The resolution result received from the ION node.
Result *resolution.Result `json:"resolutionResult"`
}

// DeactivateDID godoc
//
// @Summary Deactivate a DID document.
// @Description Deactivates a DID for which SSI is the custodian. The DID must have been previously created by calling
// @Description the "Create DID Document" endpoint. Currently, only ION dids support deactivation. The
// @Description effect of deactivating a DID is that the `didDocumentMetadata.deactivated` will be set to `true` after
// @Description doing DID resolution (e.g. by calling the `v1/dids/resolution/<did>` endpoint). Additionally, all the
// @Description 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. Please not that
// @Description deactivation 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
// @Param method path string true "Method"
// @Param id path string true "ID"
// @Param request body DeactivateDIDRequest true "request body"
// @Success 200 {object} DeactivateDIDResponse
// @Failure 400 {string} string "Bad request"
// @Failure 500 {string} string "Internal server error"
// @Router /v1/dids/{method}/{id}/deactivation [put]
func (dr DIDRouter) DeactivateDID(c *gin.Context) {
method := framework.GetParam(c, MethodParam)
if method == nil {
errMsg := "deactivate DID by method request missing method parameter"
framework.LoggingRespondErrMsg(c, errMsg, http.StatusBadRequest)
return
}
if *method != didsdk.IONMethod.String() {
framework.LoggingRespondErrMsg(c, "ion is the only method supported", http.StatusBadRequest)
}

id := framework.GetParam(c, IDParam)
if id == nil {
errMsg := fmt.Sprintf("deactivate DID request missing id parameter for method: %s", *method)
framework.LoggingRespondErrMsg(c, errMsg, http.StatusBadRequest)
return
}
var request DeactivateDIDRequest
invalidRequest := "invalid deactivate DID request"
if err := framework.Decode(c.Request, &request); err != nil {
framework.LoggingRespondErrWithMsg(c, err, invalidRequest, http.StatusBadRequest)
return
}

if err := framework.ValidateRequest(request); err != nil {
framework.LoggingRespondErrWithMsg(c, err, invalidRequest, http.StatusBadRequest)
return
}

deactivateDIDRequest, err := toDeactivateIONDIDRequest(*id)
if err != nil {
errMsg := fmt.Sprintf("%s: could not deactivate DID for method<%s>", invalidRequest, *method)
framework.LoggingRespondErrWithMsg(c, err, errMsg, http.StatusBadRequest)
return
}
deactivateIONDIDResponse, err := dr.service.DeactivateIONDID(c, *deactivateDIDRequest)
if err != nil {
errMsg := fmt.Sprintf("could not deactivate DID for method<%s>", *method)
framework.LoggingRespondErrWithMsg(c, err, errMsg, http.StatusInternalServerError)
return
}

resp := DeactivateDIDResponse{Result: deactivateIONDIDResponse.Result}
framework.Respond(c, resp, http.StatusOK)
}

func toDeactivateIONDIDRequest(id string) (*did.DeactivateIONDIDRequest, error) {
didION := ion.ION(id)
if !didION.IsValid() {
return nil, errors.Errorf("invalid ion did %s", id)
}

return &did.DeactivateIONDIDRequest{
DID: didION,
}, nil
}

type BatchCreateDIDsRequest struct {
// Required. The list of create credential requests. Cannot be more than {{.Services.DIDConfig.BatchCreateMaxItems}} items.
Requests []CreateDIDByMethodRequest `json:"requests" maxItems:"100" validate:"required,dive"`
Expand Down
1 change: 1 addition & 0 deletions pkg/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ func DecentralizedIdentityAPI(rg *gin.RouterGroup, service *didsvc.Service, did
didAPI.GET("", didRouter.ListDIDMethods)
didAPI.PUT("/:method", middleware.Webhook(webhookService, webhook.DID, webhook.Create), didRouter.CreateDIDByMethod)
didAPI.PUT("/:method/:id", didRouter.UpdateDIDByMethod)
didAPI.PUT("/:method/:id/deactivation", didRouter.DeactivateDID)
didAPI.PUT("/:method/batch", middleware.Webhook(webhookService, webhook.DID, webhook.BatchCreate), batchDIDRouter.BatchCreateDIDs)
didAPI.GET("/:method", didRouter.ListDIDsByMethod)
didAPI.GET("/:method/:id", didRouter.GetDIDByMethod)
Expand Down
65 changes: 65 additions & 0 deletions pkg/server/server_did_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,72 @@ 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)

// deactivate it
updateDIDRequest := router.DeactivateDIDRequest{}
w = httptest.NewRecorder()
params["id"] = createDIDResponse.DID.ID
requestReader = newRequestValue(tt, updateDIDRequest)
req = httptest.NewRequest(http.MethodPut, "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.DeactivateDID(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
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
}
21 changes: 4 additions & 17 deletions pkg/service/did/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ type MethodHandler interface {

// SoftDeleteDID marks the given DID as deleted. It is not removed from storage.
SoftDeleteDID(ctx context.Context, request DeleteDIDRequest) 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
Loading