Skip to content

Commit

Permalink
[MDS-5833] allow reissuance after revocation (#3025)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jsyro authored and simensma-fresh committed Jun 7, 2024
1 parent fd73cfb commit bead7db
Show file tree
Hide file tree
Showing 18 changed files with 187 additions and 113 deletions.
13 changes: 2 additions & 11 deletions docs/verifiable_credentials.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,15 +134,6 @@ Webhook processing may be inconsistent, causing messages to be processed incorre

## Local development testing

Traction DEV is configured to send webhooks to MDS DEV, and to this website for inspection
https://webhook.site/#!/view/4c0e7827-505e-47dc-9b6c-1288ac43bff5/9666e4da-5cd8-4bda-8950-6c90aa8aa29f/1
Traction DEV is configured to send webhooks to MDS DEV, and to this website for inspection https://webhook.site, after 100 requests, you must create a new testing webhook url and add that to the CPO Dev wallet on traction dev.

You can configure your local MDS to use the CPO Wallet on Traction dev as well (with env variables), but there is no way for the webhooks to get back to your local machine, so to manually test, we need to manually update our local data with the corresponding data from the webhooks.

After connection invitation acceptance:
connection_id=<UUID> and connection_state='active' need to be updated

After credential offer acceptance:
credential_state="credential_acked" and rev_reg_id=<str> and cred_rev_id=<str>

These processes did complete between the two wallet successfully, but the MDS app running on your local machine isn't getting the updates.
You can configure your local MDS to use the CPO Wallet on Traction dev as well (with env variables), but there is no way for the webhooks to get back to your local machine, so to manually test, we need to manually pass the webhook payload from traction, which will send it to webhook.site, then can be copied into Postman (or similar http client) and passed to your localhost api at `http://localhost:5000/verifiable-credentials/webhook/topic/<TOPIC>` as a json body, the topic is parameterized.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE party_verifiable_credential_connection ADD COLUMN IF NOT EXISTS last_webhook_timestamp timestamp with time zone;
ALTER TABLE party_verifiable_credential_mines_act_permit ADD COLUMN IF NOT EXISTS last_webhook_timestamp timestamp with time zone;
2 changes: 1 addition & 1 deletion services/common/src/constants/strings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,7 @@ export const MINE_REPORT_STATUS_HASH = {
[MINE_REPORT_SUBMISSION_CODES.WTD]: "Withdrawn",
};

export const VC_ACTIVE_CONNECTION_STATES = ["credential_acked", "credential-issued", "done"];
export const VC_ACTIVE_CREDENTIAL_STATES = ["credential_acked", "credential-issued", "done"];

export const CONTACTS_COUNTRY_OPTIONS = [
{ value: "CAN", label: "Canada" },
Expand Down
1 change: 1 addition & 0 deletions services/common/src/interfaces/permits/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ export * from "./permitPartyRelationship.interface";
export * from "./updatePermitAmendmentPayload.interface";
export * from "./patchPermitNumber.interface";
export * from "./draftPermitAmendment.interface";
export * from "./patchPermitVCLocked.interface";
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface IPatchPermitVCLocked {
permit_guid?: string;
mine_guid?: string;
mines_act_permit_vc_locked: boolean;
}
1 change: 1 addition & 0 deletions services/common/src/interfaces/permits/permit.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface IPermit {
permit_status_code: string;
current_permittee: string;
current_permittee_digital_wallet_connection_state: VC_CONNECTION_STATES;
mines_act_permit_vc_locked?: boolean;
current_permittee_guid: string;
project_id: string;
permit_amendments: IPermitAmendment[];
Expand Down
28 changes: 28 additions & 0 deletions services/common/src/redux/actionCreators/permitActionCreator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
IPermitCondition,
IUpdatePermitAmendmentPayload,
IPatchPermitNumber,
IPatchPermitVCLocked,
IStandardPermitCondition,
} from "@mds/common";
import { request, success, error, IDispatchError } from "../actions/genericActions";
Expand Down Expand Up @@ -446,6 +447,33 @@ export const patchPermitNumber = (
.finally(() => dispatch(hideLoading("modal")));
};

export const patchPermitVCLocked = (
permitGuid: string,
mineGuid: string,
payload: { mines_act_permit_vc_locked: boolean }
): AppThunk<Promise<IPatchPermitVCLocked | IDispatchError>> => (
dispatch
): Promise<IPatchPermitVCLocked | IDispatchError> => {
dispatch(request(reducerTypes.PATCH_PERMIT));
dispatch(showLoading("modal"));
return CustomAxios()
.patch(
`${ENVIRONMENT.apiUrl}${API.PERMITS(mineGuid)}/${permitGuid}`,
payload,
createRequestHeader()
)
.then((response: AxiosResponse<IPatchPermitVCLocked>) => {
notification.success({
message: "Successfully updated permit",
duration: 10,
});
dispatch(success(reducerTypes.PATCH_PERMIT));
return response.data;
})
.catch(() => dispatch(error(reducerTypes.PATCH_PERMIT)))
.finally(() => dispatch(hideLoading("modal")));
};

// standard permit conditions
export const fetchStandardPermitConditions = (noticeOfWorkType: string): AppThunk => (dispatch) => {
dispatch(request(reducerTypes.GET_PERMIT_CONDITIONS));
Expand Down
2 changes: 1 addition & 1 deletion services/common/src/redux/reducers/rootReducerShared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,5 @@ export const sharedReducer = {
form: formReducer,
loadingBar: loadingBarReducer,
reportSubmission: reportSubmissionReducer,
verifiableCredentialConnections: verifiableCredentialsReducer,
verifiableCredentials: verifiableCredentialsReducer,
};
29 changes: 15 additions & 14 deletions services/common/src/redux/slices/verifiableCredentialsSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,22 @@ const rejectHandler = (action) => {
console.log(action.error.stack);
};

interface VerifiableCredentialsConnection {
interface MinesActPermitVerifiableCredentialsIssuance {
party_guid: string;
permit_amendment_guid: string;
cred_exch_id: string;
cred_exch_state: string;
rev_reg_id: string;
cred_rev_id: string;
last_webhook_timestamp: Date;
}

interface VerifiableCredentialsState {
verifiableCredentialConnections: VerifiableCredentialsConnection[];
minesActPermitVerifiableCredentialsIssuance: MinesActPermitVerifiableCredentialsIssuance[];
}

const initialState: VerifiableCredentialsState = {
verifiableCredentialConnections: [],
minesActPermitVerifiableCredentialsIssuance: [],
};

const verifiableCredentialsSlice = createAppSlice({
Expand All @@ -49,7 +50,7 @@ const verifiableCredentialsSlice = createAppSlice({
},
{
fulfilled: (state, action) => {
state.verifiableCredentialConnections = action.payload.records;
state.minesActPermitVerifiableCredentialsIssuance = action.payload.records;
},
rejected: (state: VerifiableCredentialsState, action) => {
rejectHandler(action);
Expand Down Expand Up @@ -91,32 +92,32 @@ const verifiableCredentialsSlice = createAppSlice({
fulfilled: (state, action) => {
// The state here is a proxy "WritableDraft" object, so we need to convert it to a plain object
// to be able to get the current values and update the state
const verifiableCredentialConnectionsState = JSON.parse(
JSON.stringify(state.verifiableCredentialConnections)
const verifiableCredentialIssuaneState = JSON.parse(
JSON.stringify(state.minesActPermitVerifiableCredentialsIssuance)
);

state.verifiableCredentialConnections = verifiableCredentialConnectionsState.map(
(conn) => {
state.minesActPermitVerifiableCredentialsIssuance = verifiableCredentialIssuaneState
.map((conn) => {
if (conn.cred_exch_id === action.payload.credential_exchange_id) {
return { ...conn, cred_exch_state: "credential_revoked" };
}
return conn;
}
);
})
.sort((a, b) => a.last_webhook_timestamp - b.last_webhook_timestamp);
},
}
),
}),
selectors: {
getCredentialConnections: (state): VerifiableCredentialsConnection[] => {
return state.verifiableCredentialConnections;
getMinesActPermitIssuance: (state): MinesActPermitVerifiableCredentialsIssuance[] => {
return state.minesActPermitVerifiableCredentialsIssuance;
},
},
});

export const { fetchCredentialConnections, revokeCredential } = verifiableCredentialsSlice.actions;
export const { getCredentialConnections } = verifiableCredentialsSlice.getSelectors(
(rootState: RootState) => rootState.verifiableCredentialConnections
export const { getMinesActPermitIssuance } = verifiableCredentialsSlice.getSelectors(
(rootState: RootState) => rootState.verifiableCredentials
);

const verifiableCredentialsReducer = verifiableCredentialsSlice.reducer;
Expand Down
21 changes: 13 additions & 8 deletions services/core-api/app/api/mines/permits/permit/resources/permit.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ class PermitResource(Resource, UserMixin):

parser.add_argument(
'mines_act_permit_vc_locked',
type=json.dumps,
type=bool,
location='json',
store_missing=False)

Expand Down Expand Up @@ -403,15 +403,20 @@ def patch(self, permit_guid, mine_guid):
if not permit:
raise NotFound('Permit not found.')

now_application_guid = self.parser.parse_args()['now_application_guid']
now_application = NOWApplication.find_by_application_guid(now_application_guid)
now_application_guid = self.parser.parse_args().get('now_application_guid')
if now_application_guid:
now_application = NOWApplication.find_by_application_guid(now_application_guid)
if not now_application:
raise NotFound('NoW application not found')

if not now_application:
raise NotFound('NoW application not found')
if permit.permit_status_code == 'D':
#assign permit_no
permit.assign_permit_no(now_application.notice_of_work_type_code[0])

if permit.permit_status_code == 'D':
#assign permit_no
permit.assign_permit_no(now_application.notice_of_work_type_code[0])
for key, value in self.parser.parse_args().items():
if key in ['permit_no', 'mine_guid', 'uploadedFiles', 'site_properties']:
continue # non-editable fields from put or should be handled separately
setattr(permit, key, value)

permit.save()
return permit
Original file line number Diff line number Diff line change
Expand Up @@ -148,16 +148,12 @@ def vc_credential_exch_state(self):
# this will need to be revisited to support additional issuances (wallet recovery) or multple schemas issued

active = [x for x in self.vc_credential_exch if x.cred_exch_state in IssueCredentialIssuerState.active_credential_states]
pending = [x for x in self.vc_credential_exch if x.cred_exch_state in IssueCredentialIssuerState.pending_credential_states]

if active:
#if any active, return most recent
return active[0].cred_exch_state
elif pending:
#if none active, and pending, return most recent
return pending[0].cred_exch_state
else:
return None
return self.vc_credential_exch[0].cred_exch_state if len(self.vc_credential_exch) > 0 else None


def __repr__(self):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class PartyVerifiableCredentialConnection(AuditMixin, Base):
connection_id = db.Column(db.String)
connection_state = db.Column(db.String, server_default=FetchedValue())
#ARIES-RFC 0023 https://github.com/hyperledger/aries-rfcs/tree/main/features/0023-did-exchange
last_webhook_timestamp = db.Column(db.DateTime, nullable=True)


def __repr__(self):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ class PartyVerifiableCredentialMinesActPermit(AuditMixin, Base):
rev_reg_id = db.Column(db.String, nullable=True)
cred_rev_id = db.Column(db.String, nullable=True)


permit_amendment = db.relationship('PermitAmendment', lazy='select')
last_webhook_timestamp = db.Column(db.DateTime, nullable=True)


def __repr__(self):
return '<PartyVerifiableCredentialMinesActPermit cred_exch_id=%r, party_guid=%r, permit_amendment_id=%r>' % self.cred_exch_id, self.party_guid, self.permit_amendment_id
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from datetime import datetime
from flask import current_app, request
from werkzeug.exceptions import Forbidden
from flask_restx import Resource
Expand Down Expand Up @@ -31,49 +32,69 @@ def post(self, topic):
#custom auth for traction
if request.headers.get("x-api-key") != Config.TRACTION_WEBHOOK_X_API_KEY:
return Forbidden("bad x-api-key")

webhook_body = request.get_json()
current_app.logger.debug(f"TRACTION WEBHOOK <topic={topic}>: {webhook_body}")
current_app.logger.debug(f"webhook received <topic={topic}>: {webhook_body}")
if "updated_at" not in webhook_body:
current_app.logger.warn(f"webhook missing updated_at, {webhook_body}")
webhook_timestamp = datetime.fromisoformat(webhook_body["updated_at"])

if topic == CONNECTIONS:
invitation_id = webhook_body['invitation_msg_id']
vc_conn = PartyVerifiableCredentialConnection.query.unbound_unsafe().filter_by(invitation_id=invitation_id).first()
assert vc_conn, f"connection.invitation_msg_id={invitation_id} not found. webhook_body={webhook_body}"
vc_conn.connection_id = webhook_body["connection_id"]
new_state = webhook_body["state"]
if new_state != vc_conn.connection_state and vc_conn.connection_state != DIDExchangeRequesterState.COMPLETED:
# 'completed' is the final succesful state.
vc_conn.connection_state=new_state
vc_conn.save()
current_app.logger.info(f"Updated party_vc_conn connection_id={vc_conn.connection_id} with state={new_state}")
if new_state == "deleted":
# if deleted in the wallet (either in traction, or by the other agent)
vc_conn.connection_state=new_state
vc_conn.save()
current_app.logger.info(f"party_vc_conn connection_id={vc_conn.connection_id} was deleted")
if not vc_conn.connection_id:
vc_conn.connection_id = webhook_body["connection_id"]

if vc_conn.last_webhook_timestamp and vc_conn.last_webhook_timestamp >= webhook_timestamp:
current_app.logger.warn(f"webhooks out of order catch, ignoring {webhook_body}")
# already processed a more recent webhook
else:
vc_conn.last_webhook_timestamp = webhook_timestamp

new_state = webhook_body["state"]
if new_state != vc_conn.connection_state and vc_conn.connection_state != DIDExchangeRequesterState.COMPLETED:
# 'completed' is the final succesful state.
vc_conn.connection_state=new_state
vc_conn.save()
current_app.logger.info(f"Updated party_vc_conn connection_id={vc_conn.connection_id} with state={new_state}")
if new_state == "deleted":
# if deleted in the wallet (either in traction, or by the other agent)
vc_conn.connection_state=new_state
vc_conn.save()
current_app.logger.info(f"party_vc_conn connection_id={vc_conn.connection_id} was deleted")

elif topic == OUT_OF_BAND:
current_app.logger.info(f"out-of-band message invi_msg_id={webhook_body['invi_msg_id']}, state={webhook_body['state']}")

elif topic == CREDENTIAL_OFFER:
cred_exch_id = webhook_body["credential_exchange_id"]
cred_exch_record = PartyVerifiableCredentialMinesActPermit.query.unbound_unsafe().filter_by(cred_exch_id=cred_exch_id).first()

assert cred_exch_record, f"issue_credential.credential_exchange_id={cred_exch_id} not found. webhook_body={webhook_body}"
new_state = webhook_body["state"]
if new_state != cred_exch_record.cred_exch_state and cred_exch_record.cred_exch_state != "deleted":
# 'deleted' or 'credential_acked' should both be considered successful
# 'deleted' is the final state, do not update
if cred_exch_record.last_webhook_timestamp and cred_exch_record.last_webhook_timestamp >= webhook_timestamp:
current_app.logger.warn(f"webhooks out of order catch, ignoring {webhook_body}")
# already processed a more recent webhook
else:
cred_exch_record.last_webhook_timestamp = webhook_timestamp

cred_exch_record.cred_exch_state=new_state
if new_state == IssueCredentialIssuerState.CREDENTIAL_ACKED:
cred_exch_record.rev_reg_id = webhook_body["revoc_reg_id"]
cred_exch_record.cred_rev_id = webhook_body["revocation_id"]

cred_exch_record.save()
current_app.logger.info(f"Updated cred_exch_record cred_exch_id={cred_exch_id} with state={new_state}")

elif topic == ISSUER_CREDENTIAL_REVOKED:
current_app.logger.info(f"CREDENTIAL SUCCESSFULLY REVOKED received={request.get_json()}")
cred_exch = PartyVerifiableCredentialMinesActPermit.find_by_cred_exch_id(webhook_body["cred_ex_id"], unsafe=True)
cred_exch.permit_amendment.permit.mines_act_permit_vc_locked = True
cred_exch.save()

elif topic == PING:
current_app.logger.info(f"TrustPing received={request.get_json()}")

else:
current_app.logger.info(f"unknown topic '{topic}', webhook_body={webhook_body}")
current_app.logger.info(f"unknown topic '{topic}', webhook_body={webhook_body}")
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@
"cred_exch_state": fields.String,
"rev_reg_id": fields.String,
"cred_rev_id": fields.String,
"last_webhook_timestamp": fields.DateTime,
}
)
Loading

0 comments on commit bead7db

Please sign in to comment.