Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
API authz and other fixes
Benn Oshrin committed Oct 3, 2018
1 parent ecee7a1 commit c61e66b
Showing 22 changed files with 833 additions and 102 deletions.
2 changes: 1 addition & 1 deletion README.md
@@ -1,6 +1,6 @@
# COmanage Match

COmanage Match is a utility for identifying potential duplicate records from multiple authoritatize
COmanage Match is a utility for identifying potential duplicate records from multiple authoritative
systems. COmanage Match is a product of the COmanage Project.

More information is available in the [COmanage wiki](https://spaces.at.internet2.edu/display/COmanage),
25 changes: 25 additions & 0 deletions app/config/schema/schema.xml
@@ -196,4 +196,29 @@
<col>matchgrid_id</col>
</index>
</table>

<table name="api_users">
<field name="id" type="I">
<key />
<autoincrement />
</field>
<field name="matchgrid_id" type="I">
<constraint>REFERENCES matchgrids(id)</constraint>
</field>
<field name="system_of_record_id" type="I">
<constraint>REFERENCES systems_of_record(id)</constraint>
</field>
<field name="username" type="C" size="128" />
<field name="password" type="C" size="255" />
<field name="created" type="T" />
<field name="modified" type="T" />

<index name="api_users_i1">
<col>matchgrid_id</col>
</index>

<index name="api_users_i2">
<col>username</col>
</index>
</table>
</schema>
66 changes: 66 additions & 0 deletions app/src/Controller/ApiUsersController.php
@@ -0,0 +1,66 @@
<?php
/**
* COmanage Match API Users Controller
*
* Portions licensed to the University Corporation for Advanced Internet
* Development, Inc. ("UCAID") under one or more contributor license agreements.
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* UCAID licenses this file to you under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with the
* License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @link http://www.internet2.edu/comanage COmanage Project
* @package match
* @since COmanage Match v1.0.0
* @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
*/

declare(strict_types = 1);

namespace App\Controller;

class ApiUsersController extends StandardController {
/**
* Authorization for this Controller, called by Auth component
* - postcondition: $vv_permissions set with calculated permissions for this Controller
*
* @since COmanage Match v1.0.0
* @param Array $user Array of user data
* @return Boolean True if authorized for the current action, false otherwise
*/

public function isAuthorized(Array $user) {
$mgid = isset($this->cur_mg->id) ? $this->cur_mg->id : null;

$platformAdmin = $this->Authorization->isPlatformAdmin($user['username']);

$mgAdmin = $this->Authorization->isMatchAdmin($user['username'], $mgid);

if(!$platformAdmin && !$mgid) {
// Normally this is done in AppController::setMatchgrid, but since we
// allow empty Matchgrid ID we have to manually check here.
throw new \RuntimeException(__('match.er.mgid'));
}

$p = [
'add' => $platformAdmin || $mgAdmin,
'delete' => $platformAdmin || $mgAdmin,
'edit' => $platformAdmin || $mgAdmin,
'index' => $platformAdmin || $mgAdmin,
'view' => false
];

$this->set('vv_permissions', $p);
return $p[$this->request->getParam('action')];
}
}
11 changes: 9 additions & 2 deletions app/src/Controller/AppController.php
@@ -142,6 +142,8 @@ public function beforeRender(\Cake\Event\Event $event) {
*/

protected function setMatchgrid() {
// Note: TierApiController overrides this.

// $this->name = Models
$modelsName = $this->name;

@@ -162,6 +164,11 @@ protected function setMatchgrid() {
}
}

if($this->request->is('post')) {
// Accept the matchgrid ID from the posted data
$mgid = $this->request->getData('matchgrid_id');
}

if(!$mgid) {
// Try to map the requested object ID
$param = (int)$this->request->getParam('pass.0');
@@ -176,9 +183,9 @@ protected function setMatchgrid() {
}
}

if(!$mgid) {
if(!$mgid && !$this->$modelsName->allowEmptyMatchgrid()) {
// If we get this far without a Matchgrid ID, something went wrong.
throw new RuntimeException(__('match.er.mgid'));
throw new \RuntimeException(__('match.er.mgid'));
}

if($mgid) {
1 change: 1 addition & 0 deletions app/src/Controller/Component/AuthorizationComponent.php
@@ -184,6 +184,7 @@ public function menuPermissions($username, $matchgridId=null) {

return [
// Manage configuration of the current matchgrid
'api_users' => $platformAdmin || $mgAdmin,
'attributes' => $platformAdmin || $mgAdmin,
'attribute_groups' => $platformAdmin || $mgAdmin,
'rules' => $platformAdmin || $mgAdmin,
10 changes: 7 additions & 3 deletions app/src/Controller/StandardController.php
@@ -162,7 +162,7 @@ public function edit($id) {
// in afterSave
if($this->$modelsName->save($obj)) {
$this->Flash->success(__('match.rs.saved'));

return $this->generateRedirect();
}

@@ -249,7 +249,9 @@ protected function getPrimaryLink(bool $lookup=false) {
} elseif($this->request->getData($modelName . "." . $ret['linkattr'])) {
$ret['linkvalue'] = $this->request->getData($modelName . "." . $ret['linkattr']);
} else {
throw new \RuntimeException(__('match.er.primary_link', [ $ret['linkattr'] ]));
if(!$this->$modelsName->allowEmptyPrimaryLink()) {
throw new \RuntimeException(__('match.er.primary_link', [ $ret['linkattr'] ]));
}
}
}
}
@@ -275,7 +277,9 @@ public function index() {
$link = $this->getPrimaryLink();

if(!empty($link['linkattr'])) {
$query = $this->$modelsName->find()->where([$link['linkattr'] => $this->request->getQuery($link['linkattr'])]);
// If a link attribute is defined but no value is provided, then query
// where the link attribute is NULL
$query = $this->$modelsName->find()->where([$link['linkattr'].' IS' => $this->request->getQuery($link['linkattr'])]);
} else {
$query = $this->$modelsName->find();
}
137 changes: 130 additions & 7 deletions app/src/Controller/TierApiController.php
@@ -34,8 +34,9 @@

use \App\Lib\Enum\ConfidenceModeEnum;
use \App\Lib\Enum\ResolutionModeEnum;
use \App\Lib\Enum\StatusEnum;

class TierApiController extends StandardController {
class TierApiController extends AppController {
// Set by dispatched functions to control results
protected $statusCode = 500;
protected $result = [];
@@ -84,6 +85,41 @@ public function initialize() {
// $this->loadComponent('Csrf');
}

/**
* Callback run prior to the request action.
*
* @since COmanage Match v1.0.0
* @param Event $event Cake Event
*/

public function beforeFilter(\Cake\Event\Event $event) {
// We need the current matchgrid (if set) before we configuration authentication

$mgid = $this->request->getParam('matchgrid_id');

$this->loadComponent('Auth', [
'authorize' => 'Controller',
'authenticate' => [
'Basic' => [
'fields' => ['username' => 'username', 'password' => 'password'],
'userModel' => 'ApiUsers',
// Custom finder to constrain users to the request matchgrid
// We don't currently use this since Platform API users can access
// any matchgrid, but won't have the matchgrid ID set. (We could
// retrieve where $mgid||null, but we still have to filter in
// isAuthorized anyway.)
// 'finder' => ['withinMatchgrid' => ['matchgrid' => $mgid]]
// But we do want to pull SoR information for authz purposes
'finder' => 'authorization'
]
],
'storage' => 'Memory',
'unauthorizedRedirect' => false
]);

parent::beforeFilter($event);
}

/**
* Handle an API Request Current Values request, ie: GET /v1/people/sor/sorid
*
@@ -491,15 +527,76 @@ public function inventory() {
$this->render('response');
}

// XXX docblock

/**
* Authorization for this Controller, called by Auth component
* - postcondition: $vv_permissions set with calculated permissions for this Controller
*
* @since COmanage Match v1.0.0
* @param Array $user Array of user data
* @return Boolean True if authorized for the current action, false otherwise
*/

public function isAuthorized(Array $user) {
//debug('isAuthorized');
// debug($this->request->session()->read('Auth'));
return true;
Log::write('debug', 'TierApiController::isAuthorized() request for ' . $user['username']);

// Because we're using BasicAuthenticate, $user will have the record from api_users.

// This is what the API User requested:
$sor = $this->request->getParam('sor');
$mgid = (int)$this->request->getParam('matchgrid_id');

// Authorization is as follows:

// (0) Make sure the Matchgrid is active.

// By default deny access.
if(!$this->cur_mg) {
Log::write('debug', "TierApiController::isAuthorized() Requested matchgrid " . $mgid . " not found");
return false;
}

if($this->cur_mg->status != StatusEnum::Active) {
Log::write('debug', "TierApiController::isAuthorized() Requested matchgrid " . $mgid . " is not Active");
return false;
}

// (1) A Platform API user ($user['matchgrid_id'] is NULL) may perform any action.

if(!empty($user['username']) && !$user['matchgrid_id']) {
Log::write('debug', 'TierApiController::isAuthorized() ' . $user['username'] . ' authorized as Platform API User');
return true;
}

// (2) A Matchgrid API user ($user['matchgrid_id'] is NOT NULL, $user['system_of_record_id'] is NULL)
// may perform any action within the requested Matchgrid.

if(!empty($user['username'])
&& !empty($user['matchgrid_id']) // This should always be 1 or greater since SERIAL starts at 1
&& !$user['system_of_record_id'] // This is empty for Matchgrid API users
&& $user['matchgrid_id'] == $mgid) {
Log::write('debug', 'TierApiController::isAuthorized() ' . $user['username'] . ' authorized as Matchgrid API User for Matchgrid ' . $this->cur_mg->table_name . " (" . $this->cur_mg-> id . ")");
return true;
}

// (3) A System of Record API user ($user['matchgrid_id'] is NOT NULL, $user['system_of_record_id'] is NOT NULL)
// may perform any action within the requested Matchgrid + SOR.

if(!empty($user['username'])
&& !empty($user['matchgrid_id']) // This should always be 1 or greater since SERIAL starts at 1
&& !empty($user['system_of_record']['label'])
&& !empty($sor)
&& $user['matchgrid_id'] == $mgid
&& $sor == $user['system_of_record']['label']) {
Log::write('debug', 'TierApiController::isAuthorized() ' . $user['username'] . ' authorized as System of Record API User for Matchgrid ' . $this->cur_mg->table_name . " (" . $this->cur_mg-> id . "), SOR " . $user['system_of_record']['label'] . " (" . $user['system_of_record_id']. ")");
return true;
}

Log::write('debug', "TierApiController::isAuthorized() No authorization found for " . $user['username']);

// XXX These are both equivalent and generate giant stack traces in the error.log
// Can we catch them somehow and prevent them from rendering?
// Note BasicAuthenticate failure (ie: password incorrect) also dumps a stack trace
return false;
// throw new \Cake\Http\Exception\ForbiddenException();
}

/**
@@ -577,6 +674,32 @@ public function search() {
$this->doMatchRequest(true);
}

/**
* Determine the (requested) current Matchgrid and make it available to the
* rest of the application.
*
* @since COmanage Match v1.0.0
* @throws Cake\Datasource\Exception\RecordNotFoundException
* @throws \InvalidArgumentException
*/

protected function setMatchgrid() {
// This overrides (and does not call) AppController::setMatchgrid since we
// have more specific requirements here.

// For now we can just trust the passed parameter since the first thing that
// happens after we run is isAuthorized() will verify authz.
$mgid = $this->request->getParam('matchgrid_id');

if($mgid) {
$this->loadModel('Matchgrids');

// This throws Cake\Datasource\Exception\RecordNotFoundException which
// we just let pass up the stack.
$this->cur_mg = $this->Matchgrids->findById($mgid)->firstOrFail();
}
}

/**
* Handle an API Match Request request, ie: GET /v1/matchRequest/id
*

0 comments on commit c61e66b

Please sign in to comment.