1
0
mirror of https://github.com/chylex/Lightning-Tracker.git synced 2025-01-08 01:42:45 +01:00

Add tracker membership and permission system

This commit is contained in:
chylex 2020-08-05 06:52:32 +02:00
parent 4288992e3c
commit c76cb42f6b
14 changed files with 412 additions and 36 deletions

24
db/TrackerMemberTable.sql Normal file
View File

@ -0,0 +1,24 @@
CREATE TABLE `tracker_members` (
`tracker_id` INT NOT NULL,
`user_id` INT NOT NULL,
`role_id` INT NULL,
PRIMARY KEY (`tracker_id`, `user_id`),
FOREIGN KEY (`tracker_id`)
REFERENCES `trackers` (`id`)
ON UPDATE CASCADE
ON DELETE CASCADE,
FOREIGN KEY (`user_id`)
REFERENCES `users` (`id`)
ON UPDATE CASCADE
ON DELETE CASCADE,
FOREIGN KEY (`role_id`)
REFERENCES `tracker_roles` (`id`)
ON UPDATE CASCADE
ON DELETE SET NULL,
FOREIGN KEY (`role_id`, `tracker_id`) # Ensures the role-tracker pair is always valid.
REFERENCES `tracker_roles` (`id`, `tracker_id`)
ON UPDATE NO ACTION
ON DELETE NO ACTION
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE utf8mb4_general_ci

View File

@ -0,0 +1,12 @@
CREATE TABLE tracker_role_perms (
`role_id` INT NOT NULL,
`permission` ENUM (
'TODO') NOT NULL,
PRIMARY KEY (`role_id`, `permission`),
FOREIGN KEY (`role_id`)
REFERENCES tracker_roles (`id`)
ON UPDATE CASCADE
ON DELETE CASCADE
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE utf8mb4_general_ci

14
db/TrackerRoleTable.sql Normal file
View File

@ -0,0 +1,14 @@
CREATE TABLE `tracker_roles` (
`id` INT NOT NULL AUTO_INCREMENT,
`tracker_id` INT NOT NULL,
`title` VARCHAR(32) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY (`tracker_id`, `title`),
KEY (`id`, `tracker_id`), # Needed for role-tracker pair check in tracker member table.
FOREIGN KEY (`tracker_id`)
REFERENCES `trackers` (`id`)
ON UPDATE CASCADE
ON DELETE CASCADE
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE utf8mb4_general_ci

View File

@ -2,11 +2,11 @@ CREATE TABLE `trackers` (
`id` INT NOT NULL AUTO_INCREMENT,
`name` VARCHAR(32) NOT NULL,
`url` VARCHAR(32) NOT NULL,
`owner` INT NOT NULL,
`owner_id` INT NOT NULL,
`hidden` BOOL NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY (`url`),
FOREIGN KEY (`owner`)
FOREIGN KEY (`owner_id`)
REFERENCES `users` (`id`)
ON UPDATE CASCADE
ON DELETE RESTRICT

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types = 1);
namespace Database;
use Database\Filters\AbstractTrackerIdFilter;
use Database\Objects\TrackerInfo;
use PDO;
abstract class AbstractTrackerTable extends AbstractTable{
private int $tracker_id;
public function __construct(PDO $db, TrackerInfo $tracker){
parent::__construct($db);
$this->tracker_id = $tracker->getId();
}
protected function getTrackerId(): int{
return $this->tracker_id;
}
protected function prepareFilter(AbstractTrackerIdFilter $filter): AbstractTrackerIdFilter{
return $filter->internalSetTracker($this->getTrackerId());
}
}
?>

View File

@ -3,29 +3,26 @@ declare(strict_types = 1);
namespace Database\Filters;
use Database\Objects\TrackerInfo;
use PDO;
use PDOStatement;
use function Database\bind;
abstract class AbstractTrackerIdFilter extends AbstractFilter{
private ?TrackerInfo $tracker = null;
private int $tracker_id;
public function tracker(TrackerInfo $tracker): self{
$this->tracker = $tracker;
public function internalSetTracker(int $tracker_id): self{
$this->tracker_id = $tracker_id;
return $this;
}
protected function getWhereColumns(): array{
return [
'tracker_id' => $this->tracker === null ? null : self::OP_EQ
'tracker_id' => self::OP_EQ
];
}
public function prepareStatement(PDOStatement $stmt): void{
if ($this->tracker !== null){
bind($stmt, 'tracker_id', $this->tracker->getId(), PDO::PARAM_INT);
}
bind($stmt, 'tracker_id', $this->tracker_id, PDO::PARAM_INT);
}
}

View File

@ -10,8 +10,16 @@ use PDOStatement;
use function Database\bind;
final class TrackerFilter extends AbstractFilter{
public static function getUserVisibilityClause(): string{
return ' OR owner = :user_id'; // TODO
public static function getUserVisibilityClause(?string $table_name = null): string{
// TODO have roles which ban the user instead?
return
' OR '.self::field($table_name, 'owner_id').' = :user_id_1'.
' OR EXISTS(SELECT 1 FROM tracker_members tm WHERE tm.tracker_id = '.self::field($table_name, 'id').' AND tm.user_id = :user_id_2)';
}
public static function bindUserVisibility(PDOStatement $stmt, UserProfile $user): void{
bind($stmt, 'user_id_1', $user->getId(), PDO::PARAM_INT);
bind($stmt, 'user_id_2', $user->getId(), PDO::PARAM_INT);
}
public static function empty(): self{
@ -46,18 +54,18 @@ final class TrackerFilter extends AbstractFilter{
];
}
protected function generateWhereClause(): string{
$clause = parent::generateWhereClause();
protected function generateWhereClause(?string $table_name): string{
$clause = parent::generateWhereClause($table_name);
if ($this->visible_to_set){
if (!empty($clause)){
$clause .= ' AND ';
}
$clause .= ' (hidden = FALSE';
$clause .= ' ('.self::field($table_name, 'hidden').' = FALSE';
if ($this->visible_to !== null){
$clause .= self::getUserVisibilityClause();
$clause .= self::getUserVisibilityClause($table_name);
}
$clause .= ')';
@ -77,7 +85,7 @@ final class TrackerFilter extends AbstractFilter{
bind($stmt, 'url', $this->url);
if ($this->visible_to !== null){
bind($stmt, 'user_id', $this->visible_to->getId(), PDO::PARAM_INT);
self::bindUserVisibility($stmt, $this->visible_to);
}
}
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types = 1);
namespace Database\Filters\Types;
use Database\Filters\AbstractTrackerIdFilter;
final class TrackerMemberFilter extends AbstractTrackerIdFilter{
public static function empty(): self{
return new self();
}
protected function getOrderByColumns(): array{
return [
'role_order' => self::ORDER_ASC,
'user_id' => self::ORDER_DESC
];
}
}
?>

View File

@ -9,11 +9,13 @@ final class TrackerInfo{
private int $id;
private string $name;
private string $url;
private int $owner_id;
public function __construct(int $id, string $name, string $url){
public function __construct(int $id, string $name, string $url, int $owner_id){
$this->id = $id;
$this->name = $name;
$this->url = $url;
$this->owner_id = $owner_id;
}
public function getId(): int{
@ -35,6 +37,10 @@ final class TrackerInfo{
public function getUrlSafe(): string{
return protect($this->url);
}
public function getOwnerId(): int{
return $this->owner_id;
}
}
?>

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types = 1);
namespace Database\Objects;
use function Database\protect;
final class TrackerMember{
private int $user_id;
private string $user_name;
private ?string $role_title;
public function __construct(int $user_id, string $user_name, ?string $role_title){
$this->user_id = $user_id;
$this->user_name = $user_name;
$this->role_title = $role_title;
}
public function getUserId(): int{
return $this->user_id;
}
public function getUserNameSafe(): string{
return protect($this->user_name);
}
public function getRoleTitleSafe(): ?string{
return $this->role_title === null ? null : protect($this->role_title);
}
}
?>

View File

@ -0,0 +1,97 @@
<?php
declare(strict_types = 1);
namespace Database\Tables;
use Database\AbstractTrackerTable;
use Database\Filters\Types\TrackerMemberFilter;
use Database\Objects\TrackerInfo;
use Database\Objects\TrackerMember;
use PDO;
final class TrackerMemberTable extends AbstractTrackerTable{
public function __construct(PDO $db, TrackerInfo $tracker){
parent::__construct($db, $tracker);
}
public function setRole(int $user_id, int $role_id): void{
$stmt = $this->db->prepare('INSERT INTO tracker_members (tracker_id, user_id, role_id) VALUES (?, ?, ?)');
$stmt->bindValue(1, $this->getTrackerId(), PDO::PARAM_INT);
$stmt->bindValue(2, $user_id, PDO::PARAM_INT);
$stmt->bindValue(3, $role_id, PDO::PARAM_INT);
$stmt->execute();
}
public function countMembers(?TrackerMemberFilter $filter = null): ?int{
$filter = $this->prepareFilter($filter ?? TrackerMemberFilter::empty());
$stmt = $this->db->prepare('SELECT COUNT(*) FROM tracker_members '.$filter->generateClauses(true));
$filter->prepareStatement($stmt);
$stmt->execute();
$count = $this->fetchOneColumn($stmt);
return $count === false ? null : (int)$count;
}
/**
* @param TrackerMemberFilter|null $filter
* @return TrackerMember[]
*/
public function listMembers(?TrackerMemberFilter $filter = null): array{
$filter = $this->prepareFilter($filter ?? TrackerMemberFilter::empty());
$sql = <<<SQL
SELECT user_id, u.name AS user_name, role_title, role_order
FROM (
SELECT tm.user_id AS user_id,
tr.title AS role_title,
IFNULL(tm.role_id + 1, ~0) AS role_order,
tm.tracker_id AS tracker_id
FROM tracker_members tm
LEFT JOIN tracker_roles tr ON tm.role_id = tr.id
WHERE tm.tracker_id = :tracker_id_1
UNION
SELECT t.owner_id AS user_id,
'Owner' AS role_title,
0 AS role_order,
t.id AS tracker_id
FROM trackers t
WHERE t.id = :tracker_id_2
) sub
JOIN users u ON sub.user_id = u.id
SQL;
$stmt = $this->db->prepare($sql.' '.$filter->generateClauses(false, 'sub'));
$filter->prepareStatement($stmt);
$stmt->bindValue('tracker_id_1', $this->getTrackerId(), PDO::PARAM_INT);
$stmt->bindValue('tracker_id_2', $this->getTrackerId(), PDO::PARAM_INT);
$stmt->execute();
$results = [];
while(($res = $this->fetchNext($stmt)) !== false){
$results[] = new TrackerMember($res['user_id'], $res['user_name'], $res['role_title']);
}
return $results;
}
public function checkMembershipExists(int $user_id): bool{
$stmt = $this->db->prepare('SELECT 1 FROM tracker_members WHERE tracker_id = ? AND user_id = ?');
$stmt->bindValue(1, $this->getTrackerId(), PDO::PARAM_INT);
$stmt->bindValue(2, $user_id, PDO::PARAM_INT);
$stmt->execute();
return (bool)$this->fetchOneColumn($stmt);
}
public function removeUserId(int $user_id){
$stmt = $this->db->prepare('DELETE FROM tracker_members WHERE tracker_id = ? AND user_id = ?');
$stmt->bindValue(1, $this->getTrackerId(), PDO::PARAM_INT);
$stmt->bindValue(2, $user_id, PDO::PARAM_INT);
$stmt->execute();
}
}
?>

View File

@ -0,0 +1,100 @@
<?php
declare(strict_types = 1);
namespace Database\Tables;
use Database\Tables\Traits\PermTable;
use Database\AbstractTrackerTable;
use Database\Objects\RoleInfo;
use Database\Objects\TrackerInfo;
use Database\Objects\UserProfile;
use PDO;
use PDOException;
final class TrackerPermTable extends AbstractTrackerTable{
use PermTable;
private const GUEST_PERMS = []; // TODO
private const LOGON_PERMS = [];
public function __construct(PDO $db, TrackerInfo $tracker){
parent::__construct($db, $tracker);
}
protected function getDB(): PDO{
return $this->db;
}
public function addRole(string $title, array $perms): void{
$owned_transaction = !$this->db->inTransaction();
if ($owned_transaction){
$this->db->beginTransaction();
}
try{
$stmt = $this->db->prepare('INSERT INTO tracker_roles (tracker_id, title) VALUES (?, ?)');
$stmt->bindValue(1, $this->getTrackerId(), PDO::PARAM_INT);
$stmt->bindValue(2, $title);
$stmt->execute();
$this->addPermissions('INSERT INTO tracker_role_perms (role_id, permission) VALUES ()', $perms);
if ($owned_transaction){
$this->db->commit();
}
}catch(PDOException $e){
if ($owned_transaction){
$this->db->rollBack();
}
throw $e;
}
}
/**
* @return RoleInfo[]
*/
public function listRoles(): array{
$stmt = $this->db->prepare('SELECT id, title FROM tracker_roles WHERE tracker_id = ? ORDER BY id ASC');
$stmt->bindValue(1, $this->getTrackerId(), PDO::PARAM_INT);
$stmt->execute();
return $this->fetchRoles($stmt);
}
/**
* @param ?UserProfile $user
* @return string[]
*/
public function listPerms(?UserProfile $user): array{
if ($user === null){
return self::GUEST_PERMS;
}
if ($user->getRoleId() === null){
return self::LOGON_PERMS;
}
$stmt = $this->db->prepare(<<<SQL
SELECT trp.permission
FROM tracker_role_perms trp
JOIN tracker_members tm ON trp.role_id = tm.role_id
WHERE tm.user_id = ? AND tm.tracker_id = ?
SQL
);
$stmt->bindValue(1, $user->getId(), PDO::PARAM_INT);
$stmt->bindValue(2, $this->getTrackerId(), PDO::PARAM_INT);
$stmt->execute();
return $this->fetchPerms($stmt);
}
public function deleteById(int $id): void{
$stmt = $this->db->prepare('DELETE FROM tracker_roles WHERE id = ? AND tracker_id = ?');
$stmt->bindValue(1, $id, PDO::PARAM_INT);
$stmt->bindValue(2, $this->getTrackerId(), PDO::PARAM_INT);
$stmt->execute();
}
}
?>

View File

@ -8,25 +8,57 @@ use Database\Filters\Types\TrackerFilter;
use Database\Objects\TrackerInfo;
use Database\Objects\TrackerVisibilityInfo;
use Database\Objects\UserProfile;
use Exception;
use PDO;
use PDOException;
final class TrackerTable extends AbstractTable{
public function __construct(PDO $db){
parent::__construct($db);
}
/**
* @param string $name
* @param string $url
* @param bool $hidden
* @param UserProfile $owner
* @throws Exception
*/
public function addTracker(string $name, string $url, bool $hidden, UserProfile $owner): void{
$stmt = $this->db->prepare(<<<SQL
INSERT INTO trackers (name, url, hidden, owner)
VALUES (:name, :url, :hidden, :owner_id)
SQL
);
$this->db->beginTransaction();
$stmt->bindValue('name', $name);
$stmt->bindValue('url', $url);
$stmt->bindValue('hidden', $hidden, PDO::PARAM_BOOL);
$stmt->bindValue('owner_id', $owner->getId(), PDO::PARAM_INT);
$stmt->execute();
try{
$stmt = $this->db->prepare('INSERT INTO trackers (name, url, hidden, owner_id) VALUES (:name, :url, :hidden, :owner_id)');
$stmt->bindValue('name', $name);
$stmt->bindValue('url', $url);
$stmt->bindValue('hidden', $hidden, PDO::PARAM_BOOL);
$stmt->bindValue('owner_id', $owner->getId(), PDO::PARAM_INT);
$stmt->execute();
$stmt = $this->db->query('SELECT LAST_INSERT_ID()');
$stmt->execute();
$id = $this->fetchOneColumn($stmt);
if ($id === false){
$this->db->rollBack();
throw new Exception('Could not retrieve tracker ID.');
}
$tracker = new TrackerInfo($id, $name, $url, $owner->getId());
$perms = new TrackerPermTable($this->db, $tracker);
// TODO add initial permission setup
$perms->addRole('Administrator', []);
$perms->addRole('Moderator', []);
$perms->addRole('User', []);
$this->db->commit();
}catch(PDOException $e){
$this->db->rollBack();
throw $e;
}
}
public function countTrackers(TrackerFilter $filter = null): ?int{
@ -47,14 +79,14 @@ SQL
public function listTrackers(TrackerFilter $filter = null): array{
$filter ??= TrackerFilter::empty();
$stmt = $this->db->prepare('SELECT id, name, url FROM trackers '.$filter->generateClauses());
$stmt = $this->db->prepare('SELECT id, name, url, owner_id FROM trackers '.$filter->generateClauses());
$filter->prepareStatement($stmt);
$stmt->execute();
$results = [];
while(($res = $this->fetchNext($stmt)) !== false){
$results[] = new TrackerInfo($res['id'], $res['name'], $res['url']);
$results[] = new TrackerInfo($res['id'], $res['name'], $res['url'], $res['owner_id']);
}
return $results;
@ -62,17 +94,17 @@ SQL
public function getInfoFromUrl(string $url, ?UserProfile $profile): ?TrackerVisibilityInfo{
$user_visibility_clause = $profile === null ? '' : TrackerFilter::getUserVisibilityClause();
$stmt = $this->db->prepare('SELECT id, name, (hidden = FALSE'.$user_visibility_clause.') AS visible FROM trackers WHERE url = :url');
$stmt = $this->db->prepare('SELECT id, name, owner_id, (hidden = FALSE'.$user_visibility_clause.') AS visible FROM trackers WHERE url = :url');
$stmt->bindValue('url', $url);
if ($profile !== null){
$stmt->bindValue('user_id', $profile->getId());
TrackerFilter::bindUserVisibility($stmt, $profile);
}
$stmt->execute();
$res = $this->fetchOne($stmt);
return $res === false ? null : new TrackerVisibilityInfo(new TrackerInfo($res['id'], $res['name'], $url), (bool)$res['visible']);
return $res === false ? null : new TrackerVisibilityInfo(new TrackerInfo($res['id'], $res['name'], $url, $res['owner_id']), (bool)$res['visible']);
}
public function checkUrlExists(string $url): bool{

View File

@ -7,6 +7,7 @@ use Database\DB;
use Database\Objects\TrackerInfo;
use Database\Objects\UserProfile;
use Database\Tables\SystemPermTable;
use Database\Tables\TrackerPermTable;
use Exception;
use Logging\Log;
@ -40,15 +41,20 @@ final class Permissions{
}
public function checkTracker(TrackerInfo $tracker, string $permission): bool{
if ($this->user !== null && $this->user->isAdmin()){
if ($this->user !== null && ($this->user->isAdmin() || $tracker->getOwnerId() === $this->user->getId())){
return true;
}
$id = $tracker->getId();
if (!isset($this->tracker[$id])){
// TODO read
$this->tracker[$id] = [];
try{
$perms = new TrackerPermTable(DB::get(), $tracker);
$this->tracker[$id] = $perms->listPerms($this->user);
}catch(Exception $e){
Log::critical($e);
$this->tracker[$id] = [];
}
}
return in_array($permission, $this->tracker[$id]);