From 53332b989a3e4d6b24dd160fd65d9d662b7704ca Mon Sep 17 00:00:00 2001 From: Korina Cordero Date: Thu, 21 Mar 2019 14:28:18 +0000 Subject: [PATCH] Add ACL support for the API #194 --- catalyst/api-bundle/Access/Generator.php | 111 ++++++++++++++++++ catalyst/api-bundle/Access/Voter.php | 45 +++++++ .../api-bundle/Command/TestAPICommand.php | 88 ++++++++++++++ catalyst/api-bundle/Entity/User.php | 5 + config/api_acl.yaml | 39 ++++++ config/services.yaml | 16 ++- src/Access/Generator.php | 1 + src/Controller/CAPI/BatteryController.php | 14 +++ src/Controller/CAPI/VehicleController.php | 13 ++ src/Controller/CAPI/WarrantyController.php | 19 +++ 10 files changed, 350 insertions(+), 1 deletion(-) create mode 100644 catalyst/api-bundle/Access/Generator.php create mode 100644 catalyst/api-bundle/Access/Voter.php create mode 100644 catalyst/api-bundle/Command/TestAPICommand.php create mode 100644 config/api_acl.yaml diff --git a/catalyst/api-bundle/Access/Generator.php b/catalyst/api-bundle/Access/Generator.php new file mode 100644 index 00000000..7dbb0938 --- /dev/null +++ b/catalyst/api-bundle/Access/Generator.php @@ -0,0 +1,111 @@ +router = $router; + $this->cache_dir = $cache_dir; + $this->config_dir = $config_dir; + } + + public function getACL() + { + $key = 'api_access_keys'; + + // cache config + $cache_file = $this->cache_dir . '/' . $key . '.serial'; + $cache = new ConfigCache($cache_file, true); + + // check if cache is fresh + if (!$cache->isFresh()) + { + $files = []; + $resources = []; + + try + { + // get location of api_acl.yaml + $path = $this->config_dir . '/api_acl.yaml'; + + $files[] = $path; + $resources[] = new FileResource($path); + + // process api acl config file + $data = $this->parseACL($path, $key); + } + catch (\InvalidArgumentException $e) + { + error_log($e->getMessage()); + error_log($key . ' key not found in api_acl.yaml file.'); + return $data; + } + + $acl_serial = serialize($data); + $cache->write($acl_serial, $resources); + } + else + { + $acl_serial = file_get_contents($cache_file); + $data = unserialize($acl_serial); + } + + return $data; + } + + protected function parseACL($path, $key) + { + + $parser = new YamlParser(); + $config = $parser->parse(file_get_contents($path)); + + // check if we have access keys + if (!isset($config[$key])) + { + error_log('No ' . $key . ' found for ' . $path); + return; + } + + $acl_hierarchy = []; + $acl_index = []; + + // go through each one + foreach ($config[$key] as $acl_data) + { + // build hierarchy + $acl_hierarchy[$acl_data['id']] = [ + 'label' => $acl_data['label'], + 'acls' => [] + ]; + + foreach ($acl_data['acls'] as $acl) + { + $id = $acl['id']; + $label = $acl['label']; + + // set hierarchy and index + $acl_hierarchy[$acl_data['id']]['acls'][$id] = $label; + $acl_index[$id] = $label; + } + } + + return [ + 'hierarchy' => $acl_hierarchy, + 'index' => $acl_index + ]; + } +} diff --git a/catalyst/api-bundle/Access/Voter.php b/catalyst/api-bundle/Access/Voter.php new file mode 100644 index 00000000..5a5776e0 --- /dev/null +++ b/catalyst/api-bundle/Access/Voter.php @@ -0,0 +1,45 @@ +acl_gen = $acl_gen; + } + + protected function supports($attribute, $subject) + { + $acl_data = $this->acl_gen->getACL(); + + // check if the attribute is in our acl key index + if (isset($acl_data['index'][$attribute])) + return true; + + return false; + } + + protected function voteOnAttribute($attribute, $subject, TokenInterface $token) + { + $user = $token->getUser(); + + // check if any of the user's roles have access + $roles = $user->getRoleObjects(); + + foreach ($roles as $role) + { + // NOTE: ideally, we separate acl from the role object, but this will do for now + if ($role->hasACLAccess($attribute)) + return true; + } + + return false; + } +} + diff --git a/catalyst/api-bundle/Command/TestAPICommand.php b/catalyst/api-bundle/Command/TestAPICommand.php new file mode 100644 index 00000000..b7c12755 --- /dev/null +++ b/catalyst/api-bundle/Command/TestAPICommand.php @@ -0,0 +1,88 @@ +setName('api:test-connector-all') + ->setDescription('Test API connector with all commands.') + ->setHelp('Test API Connector with all commands.') + ->addArgument('protocol', InputArgument::REQUIRED, 'protocol') + ->addArgument('server', InputArgument::REQUIRED, 'server') + ->addArgument('api_key', InputArgument::REQUIRED, 'api_key') + ->addArgument('secret_key', InputArgument::REQUIRED, 'secret_key'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $protocol = $input->getArgument('protocol'); + $server = $input->getArgument('server'); + $api_key = $input->getArgument('api_key'); + $secret_key = $input->getArgument('secret_key'); + + // api client + $api = new APIClient($server, $api_key, $secret_key); + $api->setProtocol($protocol); + + // test + $api->get('/capi/test'); + + // TODO: shift this out of the bundle, since it's project specific + + + // warranty register + $serial = 'AJ34LJADR12134LKJL5'; + $plate_num = 'XEN918'; + $params = [ + 'serial' => $serial, + 'plate_number' => $plate_num, + 'warranty_class' => 'private', + 'sku' => 'WMEB24CB-CPN00-LX', + 'date_purchase' => '20181001', + 'date_expire' => '20191001', + 'first_name' => 'First', + 'last_name' => 'Last', + 'mobile_number' => '12345678910', + ]; + $api->post('/capi/warranties', $params); + + // get all warranties + $api->get('/capi/warranties'); + + + // warranty find + $api->get('/capi/warranties/' . $serial); + + // warranty claim + $id = 86811; + $serial = 'AJ34LJADR12134LKJL5'; + $params = [ + 'serial' => $serial, + ]; + $api->post('/capi/warranties/' . $id . '/claim', $params); + + // plate warranty + $api->get('/capi/plates/' . $plate_num . '/warranties'); + + // battery + $api->get('/capi/battery_brands'); + $api->get('/capi/battery_sizes'); + $api->get('/capi/batteries'); + + // vehicle + $api->get('/capi/vehicle_manufacturers'); + $api->get('/capi/vehicles'); + } +} diff --git a/catalyst/api-bundle/Entity/User.php b/catalyst/api-bundle/Entity/User.php index 778a38cf..39a5731d 100644 --- a/catalyst/api-bundle/Entity/User.php +++ b/catalyst/api-bundle/Entity/User.php @@ -106,6 +106,11 @@ class User implements UserInterface return $str_roles; } + public function getRoleObjects() + { + return $this->roles; + } + public function getDateCreate() { return $this->date_create; diff --git a/config/api_acl.yaml b/config/api_acl.yaml new file mode 100644 index 00000000..1a02da73 --- /dev/null +++ b/config/api_acl.yaml @@ -0,0 +1,39 @@ +api_access_keys: + - id: warranty + label: Warranty Access + acls: + - id: warranty.list + label: List + - id: warranty.find.serial + label: Find by Serial + - id: warranty.find.platenumber + label: Find by Plate Number + - id: warranty.register.battery + label: Register Battery + - id: warranty.claim + label: Claim + - id: batterybrand + label: Battery Brand Access + acls: + - id: batterybrand.list + label: List + - id: batterysize + label: Battery Size Access + acls: + - id: batterysize.list + label: List + - id: battery + label: Battery Access + acls: + - id: battery.list + label: List + - id: vmanufacturer + label: Vehicle Manufacturer Access + acls: + - id: vmanufacturer.list + label: List + - id: vehicle + label: Vehicle Access + acls: + - id: vehicle.list + label: List diff --git a/config/services.yaml b/config/services.yaml index 154d6708..1eb74949 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -89,5 +89,19 @@ services: tags: ['console.command'] Catalyst\APIBundle\Command\TestCommand: - tags: ['console.command'] + tags: ['console.command'] + + Catalyst\APIBundle\Access\Voter: + arguments: + $acl_gen: "@Catalyst\\APIBundle\\Access\\Generator" + tags: ['security.voter'] + + Catalyst\APIBundle\Command\TestAPICommand: + tags: ['console.command'] + + Catalyst\APIBundle\Access\Generator: + arguments: + $router: "@router.default" + $cache_dir: "%kernel.cache_dir%" + $config_dir: "%kernel.root_dir%/../config" diff --git a/src/Access/Generator.php b/src/Access/Generator.php index bb7e537b..9b46eafc 100644 --- a/src/Access/Generator.php +++ b/src/Access/Generator.php @@ -11,6 +11,7 @@ use Symfony\Component\Routing\RouterInterface; class Generator { + // TODO: make api_acl and acl yaml generator have its own bundle protected $router; protected $cache_dir; protected $config_dir; diff --git a/src/Controller/CAPI/BatteryController.php b/src/Controller/CAPI/BatteryController.php index a8197bab..cff96fb9 100644 --- a/src/Controller/CAPI/BatteryController.php +++ b/src/Controller/CAPI/BatteryController.php @@ -13,10 +13,21 @@ use App\Entity\SAPBattery; use App\Entity\SAPBatterySize; use App\Entity\SAPBatteryBrand; +use Catalyst\APIBundle\Access\Generator as ACLGenerator; + class BatteryController extends APIController { + protected $acl_gen; + + public function __construct(ACLGenerator $acl_gen) + { + $this->acl_gen = $acl_gen; + } + public function getBatteries(EntityManagerInterface $em) { + $this->denyAccessUnlessGranted('battery.list', null, 'No access.'); + $batteries = $em->getRepository(SAPBattery::class)->findBy([], ['id' => 'ASC']); $result = []; @@ -38,6 +49,7 @@ class BatteryController extends APIController public function getBrands(EntityManagerInterface $em) { + $this->denyAccessUnlessGranted('batterybrand.list', null, 'No access.'); $brands = $em->getRepository(SAPBatteryBrand::class)->findBy([], ['name' => 'ASC']); $result = []; @@ -58,6 +70,8 @@ class BatteryController extends APIController public function getSizes(EntityManagerInterface $em) { + $this->denyAccessUnlessGranted('batterysize.list', null, 'No access.'); + $sizes = $em->getRepository(SAPBatterySize::class)->findBy([], ['name' => 'ASC']); $result = []; diff --git a/src/Controller/CAPI/VehicleController.php b/src/Controller/CAPI/VehicleController.php index 0d9ab174..ab0ed286 100644 --- a/src/Controller/CAPI/VehicleController.php +++ b/src/Controller/CAPI/VehicleController.php @@ -11,10 +11,21 @@ use Catalyst\APIBundle\Response\APIResponse; use App\Entity\Vehicle; use App\Entity\VehicleManufacturer; +use Catalyst\APIBundle\Access\Generator as ACLGenerator; + class VehicleController extends APIController { + protected $acl_gen; + + public function __construct(ACLGenerator $acl_gen) + { + $this->acl_gen = $acl_gen; + } + public function getManufacturers(EntityManagerInterface $em) { + $this->denyAccessUnlessGranted('vmanufacturer.list', null, 'No access.'); + $mfgs = $em->getRepository(VehicleManufacturer::class)->findBy([], ['name' => 'ASC']); $result = []; @@ -35,6 +46,8 @@ class VehicleController extends APIController public function list(EntityManagerInterface $em) { + $this->denyAccessUnlessGranted('vehicle.list', null, 'No access.'); + $vehicles = $em->getRepository(Vehicle::class)->findBy([], ['manufacturer' => 'ASC', 'make' => 'ASC']); $result = []; diff --git a/src/Controller/CAPI/WarrantyController.php b/src/Controller/CAPI/WarrantyController.php index 3f4433a5..739cd15f 100644 --- a/src/Controller/CAPI/WarrantyController.php +++ b/src/Controller/CAPI/WarrantyController.php @@ -22,8 +22,17 @@ use App\Ramcar\WarrantyClass; use App\Ramcar\WarrantyStatus; use DateTime; +use Catalyst\APIBundle\Access\Generator as ACLGenerator; + class WarrantyController extends APIController { + protected $acl_gen; + + public function __construct(ACLGenerator $acl_gen) + { + $this->acl_gen = $acl_gen; + } + protected function cleanSerial($serial) { return trim(strtoupper($serial)); @@ -65,6 +74,8 @@ class WarrantyController extends APIController public function find($serial, EntityManagerInterface $em) { + $this->denyAccessUnlessGranted('warranty.find.serial', null, 'No access.'); + $clean_serial = $this->cleanSerial($serial); $warr = $em->getRepository(Warranty::class)->findOneBy(['serial' => $clean_serial]); @@ -81,6 +92,8 @@ class WarrantyController extends APIController public function getAll(Request $req, EntityManagerInterface $em) { + $this->denyAccessUnlessGranted('warranty.list', null, 'No access.'); + $order = $req->query->get('order'); if ($order == null) $order = 'ASC'; @@ -117,6 +130,8 @@ class WarrantyController extends APIController public function register(Request $req, EntityManagerInterface $em) { + $this->denyAccessUnlessGranted('warranty.register.battery', null, 'No access.'); + // required parameters $params = [ 'serial', @@ -220,6 +235,8 @@ class WarrantyController extends APIController public function claim(Request $req, EntityManagerInterface $em, $id) { + $this->denyAccessUnlessGranted('warranty.claim', null, 'No access.'); + // required parameters $params = [ 'serial', @@ -275,6 +292,8 @@ class WarrantyController extends APIController public function getPlateWarranties($plate_number, EntityManagerInterface $em) { + $this->denyAccessUnlessGranted('warranty.find.platenumber', null, 'No access.'); + $warranties = $em->getRepository(Warranty::class) ->findBy(['plate_number' => $plate_number], ['date_purchase' => 'DESC']);