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

[MDS-5833] allow reissuance after revocation #3025

Merged
merged 22 commits into from
Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
};
32 changes: 18 additions & 14 deletions services/common/src/redux/slices/verifiableCredentialsSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import CustomAxios from "@mds/common/redux/customAxios";
import { ENVIRONMENT, MINES_ACT_PERMITS_VC_LIST } from "@mds/common/constants";
import * as API from "@mds/common/constants/API";
import { RootState } from "@mds/common/redux/rootState";
import { DataSourceItemType } from "antd/lib/auto-complete";
import { debug } from "webpack";
import { a } from "@mds/common/tests/mocks/dataMocks";

Copy link
Collaborator

Choose a reason for hiding this comment

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

What is 'a'? And is this + debug something that's no longer being used? I would guess it might cause issues with variables on sort(a,b)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

removed

const createRequestHeader = REQUEST_HEADER.createRequestHeader;

Expand All @@ -12,21 +15,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 +53,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 +95,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
Loading