Skip to content

Commit

Permalink
Configuration to Require MFA For Login to Registry (CFM-436)
Browse files Browse the repository at this point in the history
  • Loading branch information
Benn Oshrin committed Aug 1, 2025
1 parent 2fc2846 commit e1c7cb3
Show file tree
Hide file tree
Showing 16 changed files with 339 additions and 15 deletions.
7 changes: 4 additions & 3 deletions app/config/schema/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,6 @@
},

"co_settings": {
"comment": "Table definition not yet complete (CFM-80)",

"columns": {
"id": {},
"co_id": {},
Expand All @@ -142,7 +140,10 @@
"search_global_limited_models": { "type": "boolean" },
"person_picker_email_address_type_id": { "type": "integer" },
"person_picker_identifier_type_id": { "type": "integer" },
"person_picker_display_types": { "type": "boolean" }
"person_picker_display_types": { "type": "boolean" },
"platform_env_mfa": { "type": "string", "size": 80 },
"platform_env_mfa_value": { "type": "string", "size": 80 },
"platform_env_mfa_enable_eg": { "type": "boolean" }
},
"indexes": {
"co_settings_i1": { "columns": [ "co_id" ]},
Expand Down
3 changes: 3 additions & 0 deletions app/resources/locales/en_US/command.po
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ msgstr "Launching queue runner {0} (of {1}) for CO {2}"
msgid "job.run.waiting"
msgstr "{0,plural,=1{Waiting for the last queue runner to complete} other{Waiting for # queue runners to complete}}"

msgid "mfa.reset.done"
msgstr "Removed MFA configuration for access to Registry"

msgid "opt.admin-family-name"
msgstr "Family Name of initial platform administrator"

Expand Down
3 changes: 3 additions & 0 deletions app/resources/locales/en_US/enumeration.po
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,9 @@ msgstr "Owners"
msgid "GroupTypeEnum.S"
msgstr "Standard"

msgid "GroupTypeEnum.2X"
msgstr "MFA Exempt"

msgid "GroupedAddressFieldsEnum.street,room"
msgstr "Street, Room"

Expand Down
30 changes: 30 additions & 0 deletions app/resources/locales/en_US/field.po
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,24 @@ msgstr "This determines what fields to display alongside the person name when us
msgid "CoSettings.person_picker_display_types"
msgstr "Display field types in the picker"

msgid "CoSettings.platform_env_mfa"
msgstr "MFA Assertion Indicator"

msgid "CoSettings.platform_env_mfa.desc"
msgstr "Name of the Environment Variable that indicates if MFA was asserted at login; if set, MFA must be asserted for most access to Registry"

msgid "CoSettings.platform_env_mfa_enable_eg"
msgstr "Enable MFA Exemption Groups"

msgid "CoSettings.platform_env_mfa_enable_eg.desc"
msgstr "If enabled, People placed in each CO's MFA Exemption Group will not require MFA when logging into Registry"

msgid "CoSettings.platform_env_mfa_value"
msgstr "MFA Assertion Indicator Value"

msgid "CoSettings.platform_env_mfa_value.desc"
msgstr "Expected value of the Environment Variable to indicate that MFA as asserted at login"

msgid "CoSettings.required_fields_address"
msgstr "Address Required Fields"

Expand Down Expand Up @@ -558,6 +576,9 @@ msgstr "{0} Members"
msgid "Groups.desc.members.active"
msgstr "{0} Active Members"

msgid "Groups.desc.mfaexempt"
msgstr "{0} MFA Exemptions"

msgid "Groups.group_type"
msgstr "Group Type"

Expand Down Expand Up @@ -731,6 +752,15 @@ msgstr "Default Error Landing Page"
msgid "MostlyStaticPages.default.el.body"
msgstr "An unexpected error occurred. Please contact your administrator for further assistance."

msgid "MostlyStaticPages.default.mr.title"
msgstr "MFA Required"

msgid "MostlyStaticPages.default.mr.description"
msgstr "MFA Not Provided Default Landing Page"

msgid "MostlyStaticPages.default.mr.body"
msgstr "MFA is required to access Registry, but was not signaled by your login provider. Please contact your administrator for further assistance."

msgid "MostlyStaticPages.default.pc.title"
msgstr "Enrollment Complete"

Expand Down
8 changes: 4 additions & 4 deletions app/resources/locales/en_US/information.po
Original file line number Diff line number Diff line change
Expand Up @@ -177,11 +177,11 @@ msgstr "Current version: {0}"
msgid "ug.version.target"
msgstr "Target version: {0}"

msgid "ug.tasks.createApproverGroups.co"
msgstr "Creating Approver Groups for CO {0}"
msgid "ug.tasks.createDefaultGroups.co"
msgstr "Creating new Default Groups for CO {0}"

msgid "ug.tasks.createApproverGroups.cou"
msgstr "Creating Approver Groups for COU {0}"
msgid "ug.tasks.createDefaultGroups.cou"
msgstr "Creating new Default Groups for COU {0}"

msgid "ug.tasks.installMostlyStaticPages.co"
msgstr "Installing default Mostly Static Pages for CO {0}"
Expand Down
8 changes: 4 additions & 4 deletions app/src/Command/UpgradeCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,15 @@ class UpgradeCommand extends Command
],
"5.2.0" => [
'block' => false,
'post' => ['createApproverGroups']
'post' => ['createDefaultGroups', 'installMostlyStaticPages']
]
];

// For descriptions of task parameters, see dispatch(). We store these separately
// to make them easier to use regardless of context (pre/post/manual).

protected $taskParams = [
'createApproverGroups' => ['perCO' => true, 'perCOU' => true],
'createDefaultGroups' => ['perCO' => true, 'perCOU' => true],
'installMostlyStaticPages' => ['perCO' => true]
];

Expand Down Expand Up @@ -325,14 +325,14 @@ protected function dispatch(string $task) {
}

/**
* Create Approver Groups.
* Create Approver and MFA Exemption Groups.
*
* @since COmanage Registry v5.2.0
* @param int $coId CO ID
* @param int $couId COU ID
*/

protected function createApproverGroups(int $coId, int $couId=null) {
protected function createDefaultGroups(int $coId, int $couId=null) {
$GroupsTable = $this->getTableLocator()->get('Groups');

// Technically this will try to add all the default Groups, which is fine since
Expand Down
57 changes: 57 additions & 0 deletions app/src/Controller/AppController.php
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,63 @@ public function beforeFilter(\Cake\Event\EventInterface $event) {
if(isset($this->RegistryAuth)) {
// Components might not be loaded on error, so check

// We check for MFA Indicators here since we need to do this before
// each page load, since different pages may have different requirements.

$CoSettings = TableRegistry::getTableLocator()->get('CoSettings');

$mfaConfig = $CoSettings->getMfaIndicator();

if(!empty($mfaConfig['indicator']) && !empty($mfaConfig['value'])) {
$this->llog('trace', "MFA is required for access to Registry");

if(!method_exists($this, "skipMfa")
|| !$this->skipMfa($this->request->getParam('action'))) {
// MFA is required for access to Registry, and is not skipped for this
// action, so check for it and throw an error if not asserted

if(getenv($mfaConfig['indicator']) !== $mfaConfig['value']) {
$this->llog('trace', "MFA indicator was not found");

$exempt = false;

if($mfaConfig['exempt_groups']) {
// If MFA exemption groups are enabled, figure out the MFA Exemption Group
// for the CO associated with the current request and then see if the
// Person ID associated with that CO is in that Group.

if($this->getCOID() !== null) {
$Groups = $CoSettings = TableRegistry::getTableLocator()->get('Groups');

$groupId = $Groups->getMfaExemptGroupId($this->getCOID());
$personId = $this->RegistryAuth->getPersonID($this->getCOID());

if($personId && $groupId) {
$exempt = $Groups->GroupMembers->isMember(
groupId: $groupId,
personId: $personId
);

if($exempt) {
$this->llog('trace', "Person $personId is exempt from MFA via Group $groupId");
}
}
// else we might (eg) have a Platform Admin trying to access a CO specific page
}
}

if(!$exempt) {
// We redirect to the CO's Mostly Static Page if we can figure out which CO,
// otherwise we default to the Platform one.

return $this->redirect("/" . ($this->getCOID() ?? $mfaConfig['comanage_co_id']) . "/mfa-required");
}
}
} else {
$this->llog('trace', "MFA check is skipped for this request (" . $this->name . ")");
}
}

// We need to populate this in beforeFilter (rather than beforeRender)
// so it's available to CosController::select
$this->populateAvailableCos();
Expand Down
33 changes: 33 additions & 0 deletions app/src/Controller/EnrollmentFlowsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,39 @@ public function copy(string $id) {
return $this->generateRedirect(null);
}

/**
* Determine if MFA, if otherwise required, is not required for this action.
*
* @since COmanage Registry v5.2.0
* @param string $action Controller action
* @return bool true if MFA can be skipped, false otherwise
*/

public function skipMfa(string $action): bool {
if($action == 'start') {
// We allow unregistered identities (any actor type other than "person")
// to skip MFA because they might not have MFA until they finish enrollment.
// (This is similar to PetitionsController::dispatch().) Note that since this
// is the 'start' call we don't yet have a Petition ID.

$actorInfo = $this->getCurrentActor();

if(!empty($actorInfo['type']) && $actorInfo['type'] != 'person') {
// This implies that someone who is registered on the platform but NOT enrolled
// in the current CO (including, potentially, Platform Admins, will be exempt from MFA).
// This is probably ok since these people presumably don't have any authorization,
// and if anything are trying to run an enrollment. One exception is Platform Admins,
// so we check for that specifically.

if(!in_array('cmpadmin', $actorInfo['roles'])) {
return true;
}
}
}

return false;
}

/**
* Start an Enrollment Flow.
*
Expand Down
12 changes: 12 additions & 0 deletions app/src/Controller/PagesController.php
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,18 @@ public function show(string $coid, string $name) {
return $this->render('/MostlyStaticPages/display');
}

/**
* Determine if MFA, if otherwise required, is not required for this action.
*
* @since COmanage Registry v5.2.0
* @param string $action Controller action
* @return bool true if MFA can be skipped, false otherwise
*/

public function skipMfa(string $action): bool {
return in_array($action, ['display', 'show']);
}

/**
* Indicate whether this Controller will handle some or all authnz.
*
Expand Down
30 changes: 30 additions & 0 deletions app/src/Controller/PetitionsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,36 @@ public function resume(string $id) {
}
}

/**
* Determine if MFA, if otherwise required, is not required for this action.
*
* @since COmanage Registry v5.2.0
* @param string $action Controller action
* @return bool true if MFA can be skipped, false otherwise
*/

public function skipMfa(string $action): bool {
if($action == 'dispatch') {
// We allow unregistered identities (any actor type other than "person")
// to skip MFA because they might not have MFA until they finish enrollment.
// However, we want to enforce MFA for registered People who might (eg) be
// approving a Petition. (There is a similar check in EnrollmentFlowsController::start().)
// We call getActorInfo without a Petition ID because we only need the person type.

$actorInfo = $this->getCurrentActor();

if(!empty($actorInfo['type']) && $actorInfo['type'] != 'person') {
// Perform the same check for Platform admins as PetitionsController.

if(!in_array('cmpadmin', $actorInfo['roles'])) {
return true;
}
}
}

return false;
}

/**
* Terminate an in-progress Petition.
*
Expand Down
1 change: 1 addition & 0 deletions app/src/Lib/Enum/GroupTypeEnum.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class GroupTypeEnum extends StandardEnum {
const AllMembers = 'M';
// Note that other groups can be used for Approval, this is the _default_ group for the CO/COU
const Approvers = 'AP';
const MfaExempt = '2X';
const Owners = 'O';
const Standard = 'S';
}
14 changes: 14 additions & 0 deletions app/src/Model/Entity/MostlyStaticPage.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,19 @@ class MostlyStaticPage extends Entity {
'slug' => false,
];

/**
* Determine if this entity record can be deleted.
*
* @since COmanage Registry v5.2.0
* @return bool True if the record can be deleted, false otherwise
*/

public function canDelete(): bool {
// AR-MostlyStaticPage-3 Default Pages can not be deleted, or have their names, status,
// or context changed.
return !$this->isDefaultPage();
}

/**
* Determine if this entity is a default Page (shipped out of the box and relied on by other
* parts of the Application).
Expand All @@ -55,6 +68,7 @@ public function isDefaultPage(): bool {
'default-handoff',
'duplicate-landing',
'error-landing',
'mfa-required',
'petition-complete'
]);
}
Expand Down
Loading

0 comments on commit e1c7cb3

Please sign in to comment.