diff --git a/.gitignore b/.gitignore index e30050e844a21dea513d3475a784248b714105d8..13cd4245dc140b4152008469b035ec904a5f00a8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ composer.lock config.php logs/ vendor/ +client-icons/ +/nbproject/ diff --git a/classes/DAO.php b/classes/DAO.php index c5d5039b41e0661053affd93f8f5f9e64ea9edd3..d72d3b9b9156cb45fc3f62553cd3ea04aa80b675 100644 --- a/classes/DAO.php +++ b/classes/DAO.php @@ -124,4 +124,12 @@ interface DAO { * @param type $token join token */ function deleteJoinRequest($token); + + /** + * CRUD methods for OAuth2Clients (used by admin interface). + */ + function getOAuth2Clients(); + function createOAuth2Client($client); + function updateOAuth2Client($client); + function deleteOAuth2Client($clientId); } diff --git a/classes/MySQLDAO.php b/classes/MySQLDAO.php index f81186d7ddb042f8046157c22bd6209d90282f7b..67511697b24fd8f3e28472897ef31c099bc3d1ce 100644 --- a/classes/MySQLDAO.php +++ b/classes/MySQLDAO.php @@ -332,4 +332,150 @@ class MySQLDAO implements DAO { $stmt->execute(); } + function getOAuth2Clients() { + $dbh = $this->getDBHandler(); + + // Load clients info + $queryClient = "SELECT id, name, icon, client, secret, redirect_url, scope FROM oauth2_client"; + $stmtClients = $dbh->prepare($queryClient); + $stmtClients->execute(); + + $clientsMap = []; + + foreach ($stmtClients->fetchAll() as $row) { + $client = new OAuth2Client(); + $client->id = $row['id']; + $client->name = $row['name']; + $client->icon = $row['icon']; + $client->client = $row['client']; + $client->secret = $row['secret']; + $client->redirectUrl = $row['redirect_url']; + $client->scope = $row['scope']; + $clientsMap[$client->id] = $client; + } + + // Load authentication methods info + $queryAuthNMethods = "SELECT client_id, auth_method FROM oauth2_client_auth_methods"; + + $stmtAuthNMethods = $dbh->prepare($queryAuthNMethods); + $stmtAuthNMethods->execute(); + + foreach ($stmtAuthNMethods->fetchAll() as $row) { + $id = $row['client_id']; + array_push($clientsMap[$id]->authMethods, $row['auth_method']); + } + + $clients = []; + foreach ($clientsMap as $id => $client) { + array_push($clients, $client); + } + + return $clients; + } + + function createOAuth2Client($client) { + $dbh = $this->getDBHandler(); + + try { + $dbh->beginTransaction(); + + $stmt = $dbh->prepare("INSERT INTO `oauth2_client`(`name`, `icon`, `client`, `secret`, `redirect_url`, `scope`)" + . " VALUES(:name, :icon, :client, :secret, :redirect_url, :scope)"); + + $stmt->bindParam(':name', $client->name); + $stmt->bindParam(':icon', $client->icon); + $stmt->bindParam(':client', $client->client); + $stmt->bindParam(':secret', $client->secret); + $stmt->bindParam(':redirect_url', $client->redirectUrl); + $stmt->bindParam(':scope', $client->scope); + + $stmt->execute(); + + $client->id = $dbh->lastInsertId(); + + foreach ($client->authMethods as $method) { + $stmt = $dbh->prepare("INSERT INTO `oauth2_client_auth_methods`(`client_id`, `auth_method`)" + . " VALUES(:client_id, :auth_method)"); + + $stmt->bindParam(':client_id', $client->id); + $stmt->bindParam(':auth_method', $method); + + $stmt->execute(); + } + + $dbh->commit(); + } catch (Exception $ex) { + $dbh->rollBack(); + throw $ex; + } + + return $client; + } + + function updateOAuth2Client($client) { + $dbh = $this->getDBHandler(); + + try { + $dbh->beginTransaction(); + + $stmt = $dbh->prepare("UPDATE `oauth2_client` SET `name` = :name, `icon` = :icon, " + . " `client` = :client, `secret` = :secret, `redirect_url` = :redirect_url, `scope` = :scope " + . " WHERE id = :id"); + + $stmt->bindParam(':name', $client->name); + $stmt->bindParam(':icon', $client->icon); + $stmt->bindParam(':client', $client->client); + $stmt->bindParam(':secret', $client->secret); + $stmt->bindParam(':redirect_url', $client->redirectUrl); + $stmt->bindParam(':scope', $client->scope); + $stmt->bindParam(':id', $client->id); + + $stmt->execute(); + + // Delete old authentication methods + $stmt = $dbh->prepare("DELETE FROM oauth2_client_auth_methods WHERE client_id = :id"); + $stmt->bindParam(':id', $client->id); + + $stmt->execute(); + + // Re-add authentication methods + foreach ($client->authMethods as $method) { + $stmt = $dbh->prepare("INSERT INTO `oauth2_client_auth_methods`(`client_id`, `auth_method`)" + . " VALUES(:client_id, :auth_method)"); + + $stmt->bindParam(':client_id', $client->id); + $stmt->bindParam(':auth_method', $method); + + $stmt->execute(); + } + + $dbh->commit(); + } catch (Exception $ex) { + $dbh->rollBack(); + throw $ex; + } + + return $client; + } + + function deleteOAuth2Client($clientId) { + $dbh = $this->getDBHandler(); + try { + $dbh->beginTransaction(); + + $stmt = $dbh->prepare("DELETE FROM `oauth2_client_auth_methods` WHERE client_id = :id"); + $stmt->bindParam(':id', $clientId); + $stmt->execute(); + + $stmt = $dbh->prepare("DELETE FROM `oauth2_client` WHERE id = :id"); + $stmt->bindParam(':id', $clientId); + $stmt->execute(); + + $dbh->commit(); + } catch (Exception $ex) { + $dbh->rollBack(); + throw $ex; + } + } + } diff --git a/classes/OAuth2Client.php b/classes/OAuth2Client.php new file mode 100644 index 0000000000000000000000000000000000000000..a00cf7ad5b3db5cc334a115857a8cb7cc925c7d3 --- /dev/null +++ b/classes/OAuth2Client.php @@ -0,0 +1,42 @@ +<?php + +/* ---------------------------------------------------------------------------- + * INAF - National Institute for Astrophysics + * IRA - Radioastronomical Institute - Bologna + * OATS - Astronomical Observatory - Trieste + * ---------------------------------------------------------------------------- + * + * Copyright (C) 2019 Istituto Nazionale di Astrofisica + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License Version 3 as published by the + * Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace RAP; + +/** + * Data model for storing information about a RAP client connecting using OAuth2. + */ +class OAuth2Client { + + public $id; + public $name; + public $icon; + public $client; + public $secret; + public $redirectUrl; + public $scope; + // list of AuthN methods + public $authMethods = []; + +} diff --git a/include/admin.php b/include/admin.php new file mode 100644 index 0000000000000000000000000000000000000000..6423d3494e09f0c6a6693daf4b6a01b1abfdfc9c --- /dev/null +++ b/include/admin.php @@ -0,0 +1,91 @@ +<?php + +/** + * Functionalities for the admin panel. + */ +// + +function checkUser() { + + startSession(); + + global $session; + if ($session->user === null) { + http_response_code(401); + die("You must be registered to perform this action"); + } + + // TODO: check is admin +} + +Flight::route('GET /admin', function() { + global $VERSION; + Flight::render('admin/index.php', array('title' => 'Admin panel', + 'version' => $VERSION)); +}); + +Flight::route('GET /admin/oauth2_clients', function() { + + checkUser(); + global $dao; + + $clients = $dao->getOAuth2Clients(); + + Flight::json($clients); +}); + +Flight::route('POST /admin/oauth2_clients', function() { + + checkUser(); + global $dao; + + $client = $dao->createOAuth2Client(buildOAuth2ClientFromData()); + + Flight::json($client); +}); + +Flight::route('PUT /admin/oauth2_clients', function() { + + checkUser(); + global $dao; + + $client = $dao->updateOAuth2Client(buildOAuth2ClientFromData()); + + Flight::json($client); +}); + +Flight::route('DELETE /admin/oauth2_clients/@id', function($id) { + + checkUser(); + global $dao; + + $dao->deleteOAuth2Client($id); + + // Return no content + Flight::halt(204); +}); + +function buildOAuth2ClientFromData() { + + $data = Flight::request()->data; + $client = new \RAP\OAuth2Client(); + + if (isset($data)) { + if (isset($data['id'])) { + $client->id = $data['id']; + } + $client->name = $data['name']; + $client->icon = $data['icon']; + $client->client = $data['client']; + $client->secret = $data['secret']; + $client->redirectUrl = $data['redirectUrl']; + $client->scope = $data['scope']; + } + if (isset($data['authMethods'])) { + foreach ($data['authMethods'] as $method) { + array_push($client->authMethods, $method); + } + } + + return $client; +} diff --git a/include/front-controller.php b/include/front-controller.php index 1fbabb77224c1ad6fe6b71ec73fc299ca40cd100..463ff21308ae5c9a968ecc89d2813d09a3291806 100644 --- a/include/front-controller.php +++ b/include/front-controller.php @@ -222,3 +222,5 @@ Flight::route('GET /register', function() { $callbackHandler->manageLoginRedirect($user, $session); } }); + +include 'admin.php'; \ No newline at end of file diff --git a/include/gui-backend.php b/include/gui-backend.php index 44e3eda9384dd4bb5aa03b63734d7cdda528a41d..5cc8dcf023202197c9d9239846bf59b80db13a34 100644 --- a/include/gui-backend.php +++ b/include/gui-backend.php @@ -32,7 +32,7 @@ Flight::route('GET /user', function() { array_push($jsRes, $searchResult->userDisplayText); } - echo json_encode($jsRes); + Flight::json($jsRes); }); Flight::route('POST /join', function() { diff --git a/include/rest-web-service.php b/include/rest-web-service.php index b0ffc190795cd8c4b0d6c1cd9b7d6ff73b624c5e..e1590e839afd8035b189b86cd70169285e92405b 100644 --- a/include/rest-web-service.php +++ b/include/rest-web-service.php @@ -46,8 +46,7 @@ Flight::route('GET ' . $WS_PREFIX . '/user/@userId', function($userId) { $user = $dao->findUserById($userId); if ($user !== null) { - header('Content-Type: application/json'); - echo json_encode($user); + Flight::json($user); } else { http_response_code(404); die("User not found"); @@ -63,7 +62,7 @@ Flight::route('GET ' . $WS_PREFIX . '/user', function() { $searchText = Flight::request()->query['search']; $users = $dao->searchUser($searchText); - echo json_encode($users); + Flight::json($users); }); /** @@ -98,7 +97,7 @@ Flight::route('POST ' . $WS_PREFIX . '/user', function() { $userHandler->saveUser($user); - echo json_encode($user); + Flight::json($user); }); /** diff --git a/js/admin.js b/js/admin.js new file mode 100644 index 0000000000000000000000000000000000000000..ba5d8f0a4e46c9ba0e48df0d74e90b719f8d0c5b --- /dev/null +++ b/js/admin.js @@ -0,0 +1,160 @@ +(function () { + + var AUTH_METHODS = ['eduGAIN', 'Google', 'Facebook', 'LinkedIn', 'X.509', 'LocalIdP']; + + var vm = new Vue({ + el: '#admin-vue', + data: { + oauth2Clients: [], + oauth2ClientToDelete: null + }, + methods: { + addNewOAuth2Client: function () { + this.$data.oauth2Clients.push(getNewClient()); + }, + editOAuth2Client: function (client) { + client.edit = true; + vm.$forceUpdate(); + }, + getAuthMethodsString: function (authMethods) { + var selectedValues = []; + for (var i = 0; i < AUTH_METHODS.length; i++) { + var method = AUTH_METHODS[i]; + if (authMethods[method] === true) { + selectedValues.push(method); + } + } + return selectedValues.join(', '); + }, + saveOAuth2Client: function (client, index) { + if (client.id === null) { + createOAuth2Client(client, index); + } else { + updateOAuth2Client(client, index); + } + }, + askConfirmDeleteOAuth2Client: function (client, index) { + vm.$data.oauth2ClientToDelete = index; + if (client.id === null) { + deleteOAuth2Client(); + } else { + $('#client-to-delete').text(client.name); + $('#confirm-delete-client-modal').modal('show'); + } + } + } + }); + + function getNewClient() { + var client = { + id: null, + name: null, + icon: null, + client: null, + secret: null, + redirectUrl: null, + scope: null, + authMethods: {}, + edit: true + }; + for (var i = 0; i < AUTH_METHODS.length; i++) { + client.authMethods[AUTH_METHODS[i]] = true; + } + return client; + } + + /* Converts the model received from the server into a model + * useful for the Vue manipulation */ + function getJsModel(client) { + var jsAuthMethods = {}; + for (var i = 0; i < AUTH_METHODS.length; i++) { + var method = AUTH_METHODS[i]; + jsAuthMethods[method] = client.authMethods.includes(method); + } + client.authMethods = jsAuthMethods; + client.edit = false; + return client; + } + + /* Converts the model manipulated by Vue in the model expected by the back-end */ + function getBackendModel(client) { + var client = Vue.util.extend({}, client); + var authMethods = []; + for (var i = 0; i < AUTH_METHODS.length; i++) { + var method = AUTH_METHODS[i]; + if (client.authMethods[method] === true) { + authMethods.push(method); + } + } + client.authMethods = authMethods; + delete client.edit; + return client; + } + + function createOAuth2Client(client, index) { + showWaiting(); + $.ajax({ + url: 'admin/oauth2_clients', + method: 'POST', + dataType: 'json', + contentType: 'application/json', + data: JSON.stringify(getBackendModel(client)) + }).then(function (data) { + vm.$data.oauth2Clients[index] = getJsModel(data); + vm.$forceUpdate(); + hideWaiting(); + }); + } + + function updateOAuth2Client(client, index) { + showWaiting(); + $.ajax({ + url: 'admin/oauth2_clients', + method: 'PUT', + dataType: 'json', + contentType: 'application/json', + data: JSON.stringify(getBackendModel(client)) + }).then(function (data) { + vm.$data.oauth2Clients[index] = getJsModel(data); + vm.$forceUpdate(); + hideWaiting(); + }); + } + + + function deleteOAuth2Client() { + var client = vm.$data.oauth2Clients[vm.$data.oauth2ClientToDelete]; + if (client.id === null) { + vm.$data.oauth2Clients.splice(vm.$data.oauth2ClientToDelete, 1); + vm.$forceUpdate(); + } else { + showWaiting(); + $.ajax({ + url: 'admin/oauth2_clients/' + client.id, + method: 'DELETE' + }).then(function () { + vm.$data.oauth2Clients.splice(vm.$data.oauth2ClientToDelete, 1); + $('#confirm-delete-client-modal').modal('hide'); + vm.$forceUpdate(); + hideWaiting(); + }); + } + } + $(document).on('click', '#confirm-delete-client', deleteOAuth2Client); + + showWaiting(); + + $.ajax({ + url: 'admin/oauth2_clients', + method: 'GET', + dataType: 'json' + }).then(function (data) { + var clients = []; + for (var i = 0; i < data.length; i++) { + clients.push(getJsModel(data[i])); + } + vm.$data.oauth2Clients = clients; + hideWaiting(); + }); + +})(); \ No newline at end of file diff --git a/sql/setup-database.sql b/sql/setup-database.sql index d5b4328d9e15973e4f0dbdd1a026cfca934dd987..d9cd3f367c84a07eba5c83d669e7d029663c7bdc 100644 --- a/sql/setup-database.sql +++ b/sql/setup-database.sql @@ -1,3 +1,21 @@ +CREATE TABLE `oauth2_client` ( + `id` int NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL, + `icon` varchar(255), + `client` varchar(255) NOT NULL, + `secret` varchar(255) NOT NULL, + `redirect_url` text NOT NULL, + `scope` varchar(255) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE `oauth2_client_auth_methods` ( + `client_id` int NOT NULL, + `auth_method` varchar(255) NOT NULL, + PRIMARY KEY (`client_id`, `auth_method`), + FOREIGN KEY (`client_id`) REFERENCES `oauth2_client`(`id`) +); + CREATE TABLE `user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `primary_identity` bigint(20) DEFAULT NULL, diff --git a/views/admin/index.php b/views/admin/index.php new file mode 100644 index 0000000000000000000000000000000000000000..16c4f5b3a13c729c9d4f5b815697f86ba89cd664 --- /dev/null +++ b/views/admin/index.php @@ -0,0 +1,127 @@ +<?php + +include 'include/header.php'; +?> + +<script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js"></script> + +<h1 class="text-center">Admin panel</h1> + +<div id="admin-vue"> + <button class="btn btn-success pull-right" v-on:click="addNewOAuth2Client"> + <span class="glyphicon glyphicon-plus-sign"></span> Add client + </button> + <h2>OAuth2 clients</h2> + <br/> + <div class="panel panel-default" v-for="(client, index) in oauth2Clients"> + <div class="panel-heading"> + <button class="btn btn-danger pull-right" v-on:click="askConfirmDeleteOAuth2Client(client, index)"> + <span class="glyphicon glyphicon-trash"></span> Delete + </button> + <button class="btn btn-primary pull-right" v-on:click="editOAuth2Client(client)" v-if="!client.edit"> + <span class="glyphicon glyphicon-pencil"></span> Edit + </button> + <button class="btn btn-primary pull-right" v-on:click="saveOAuth2Client(client, index)" v-if="client.edit"> + <span class="glyphicon glyphicon-floppy-disk"></span> Save + </button> + <strong>{{client.name}}</strong> + </div> + <div class="panel-body"> + <div class="form-horizontal"> + <div class="form-group"> + <label class="col-sm-3 control-label" for="name">Name</label> + <div class="col-sm-9"> + <p class="form-control-static" v-if="!client.edit">{{client.name}}</p> + <input type="text" class="form-control" id="name" v-model="client.name" v-if="client.edit" /> + </div> + </div> + <div class="form-group"> + <label class="col-sm-3 control-label">Icon</label> + <div class="col-sm-9"> + <p class="form-control-static" v-if="!client.edit">{{client.icon}}</p> + </div> + </div> + <div class="form-group"> + <label class="col-sm-3 control-label" for="client_id">Client id</label> + <div class="col-sm-9"> + <p class="form-control-static" v-if="!client.edit">{{client.client}}</p> + <input type="text" class="form-control" id="client_id" v-model="client.client" v-if="client.edit" /> + </div> + </div> + <div class="form-group"> + <label class="col-sm-3 control-label" for="client_secret">Client Secret</label> + <div class="col-sm-9"> + <p class="form-control-static" v-if="!client.edit">{{client.secret}}</p> + <input type="text" class="form-control" id="client_secret" v-model="client.secret" v-if="client.edit" /> + </div> + </div> + <div class="form-group"> + <label class="col-sm-3 control-label" for="redirect_url">Redirect URL</label> + <div class="col-sm-9"> + <p class="form-control-static" v-if="!client.edit">{{client.redirectUrl}}</p> + <input type="text" class="form-control" id="redirect_url" v-model="client.redirectUrl" v-if="client.edit" /> + </div> + </div> + <div class="form-group"> + <label class="col-sm-3 control-label" for="scope">Scope</label> + <div class="col-sm-9"> + <p class="form-control-static" v-if="!client.edit">{{client.scope}}</p> + <input type="text" class="form-control" id="scope" v-model="client.scope" v-if="client.edit" /> + </div> + </div> + <div class="form-group"> + <label class="col-sm-3 control-label" for="authenticationMethods">Authentication methods</label> + <div class="col-sm-9"> + <p class="form-control-static" v-if="!client.edit">{{getAuthMethodsString(client.authMethods)}}</p> + <div v-if="client.edit"> + <div class="checkbox"> + <label> + <input type="checkbox" v-model="client.authMethods['eduGAIN']"> eduGAIN + </label> + <label> + <input type="checkbox" v-model="client.authMethods['Google']"> Google + </label> + <label> + <input type="checkbox" v-model="client.authMethods['Facebook']"> Facebook + </label> + <label> + <input type="checkbox" v-model="client.authMethods['LinkedIn']"> LinkedIn + </label> + <label> + <input type="checkbox" v-model="client.authMethods['X.509']"> X.509 + </label> + <label> + <input type="checkbox" v-model="client.authMethods['LocalIdP']"> LocalIdP + </label> + </div> + </div> + </div> + </div> + </div> + </div> + </div> +</div> + +<script src="js/admin.js"></script> + +<div class="modal fade" tabindex="-1" role="dialog" id="confirm-delete-client-modal"> + <div class="modal-dialog" role="document"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button> + <h4 class="modal-title">Confirm action</h4> + </div> + <div class="modal-body"> + <p>Are you sure that you want to delete the client <span id="client-to-delete"></span>?</p> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-danger" id="confirm-delete-client">Delete</button> + <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button> + </div> + </div> + </div> +</div> + +<?php + +include 'include/footer.php';