diff --git includes/VersioncontrolRepository.php includes/VersioncontrolRepository.php index 0d8097c..1fd8ede 100644 --- includes/VersioncontrolRepository.php +++ includes/VersioncontrolRepository.php @@ -476,6 +476,19 @@ abstract class VersioncontrolRepository implements VersioncontrolEntityInterface return new $class_name(); } + public function getAuthHandler() { + if (!isset($this->pluginInstances['auth_handler'])) { + // If no plugin is set, use the free-for-all plugin + if (empty($this->plugins['auth_handler'])) { + // FIXME temporarily writing to a db-recorded field like this is very hacky + $this->plugins['auth_handler'] = 'ffa'; + } + $this->pluginInstances['auth_handler'] = $this->getPluginClass('auth_handler', 'authorization', 'handler'); + $this->pluginInstances['auth_handler']->setRepository($this); + } + return $this->pluginInstances['auth_handler']; + } + public function getAuthorMapper() { if (!isset($this->pluginInstances['author_mapper'])) { // if no plugin is set, just directly register FALSE for the instance diff --git includes/interfaces.inc includes/interfaces.inc index 3d0114e..dfa8295 100644 --- includes/interfaces.inc +++ includes/interfaces.inc @@ -203,4 +203,22 @@ interface VersioncontrolUserMapperInterface { * Either a uid (int), or FALSE if the mapping failed. */ public function mapCommitter(VersioncontrolOperation $commit); +} + +interface VersioncontrolAuthHandlerInterface { + public function setRepository(VersioncontrolRepository $repository); + /** + * Determine whether this user has any access at all to the repository. + * + * Implementing code should always check this first to get around having to + * do more complex checks. + */ + public function authAccess(object $user); + public function authBranchCreate(object $user); + public function authBranchDelete(object $user, VersioncontrolBranch $branch); + public function authBranchUpdate(object $user, VersioncontrolBranch $branch); + public function authTagCreate(object $user); + public function authTagDelete(object $user, VersioncontrolTag $tag); + public function authTagUpdate(object $user, VersioncontrolTag $tag); + public function getErrorMessages(); } \ No newline at end of file diff --git includes/plugins/vcs_auth/VersioncontrolAuthHandlerFFA.class.php includes/plugins/vcs_auth/VersioncontrolAuthHandlerFFA.class.php new file mode 100644 index 0000000..d1e87fc --- /dev/null +++ includes/plugins/vcs_auth/VersioncontrolAuthHandlerFFA.class.php @@ -0,0 +1,32 @@ +repoSet && $this->repository !== $repository) { + throw new Exception('Cannot attach different repositories to a single VersioncontrolAuthHandlerMappedAccounts instance. Instanciate a new object.', E_RECOVERABLE_ERROR); + } + + $this->repository = $repository; + $this->repoSet = TRUE; + $this->build(); + } + + protected function build() { + if ($this->built) { + return; // already built, bail out + } + if (empty($this->repoSet)) { + throw new Exception('Cannot build the account mapper object until a repository has been attached.'); + } + + // Retrieve the base auth data + $this->userData = db_select('versioncontrol_auth_account') + ->fields('base') + ->condition('repo_id', $this->repository->repo_id) + ->execute() + ->fetchAllAssoc('uid'); + + foreach ($this->userData as &$data) { + $data['per-label-auth'] = array(); + } + + // Retrieve the extended per-label auth data + $label_data = db_select('versioncontrol_auth_account_label') + ->fields('base') + ->condition('repo_id', $this->repository->repo_id) + ->execute(); + + foreach ($label_data as $row) { + $labeldata = array( + 'update' => $row->update, + 'delete' => $row->delete, + ); + $this->userData[$row->uid]['per-label-auth'][$row->label_id] = $labeldata; + } + + $this->built = TRUE; + } + + public function authAccess(object $user) { + $this->build(); + if (empty($this->userData[$user->uid]) || empty($this->userData[$user->uid]['access'])) { + // No account is registered, or access is set to 0 on the account + $this->errors[] = t('%name does not have access to this repository.', array('name' => $user->name)); + return FALSE; + } + return TRUE; + } + + protected function baseAuth(object $user) { + $this->build(); + if (empty($this->userData[$user->uid])) { + // If no record of the user, deny. + return self::DENY; + } + else { + return (int) $this->userData[$user->uid]['access']; + } + } + + public function authBranchCreate(object $user) { + $base = $this->baseAuth($user); + if ($base == self::DENY) { + // Zero access, deny. + return FALSE; + } + else if ($base == self::ALL) { + // User has super cow powers, say yes. + return TRUE; + } + + return $this->userData[$user->uid]['branch_create'] == self::GRANT; + } + + public function authBranchDelete(object $user, VersioncontrolBranch $branch) { + return $this->authLabel($user, $branch, 'delete'); + } + + public function authBranchUpdate(object $user, VersioncontrolBranch $branch) { + return $this->authLabel($user, $branch, 'update'); + } + public function authTagCreate(object $user) { + $base = $this->baseAuth($user); + if ($base == self::DENY) { + // Zero access, deny. + return FALSE; + } + else if ($base == self::ALL) { + // User has super cow powers, say yes. + return TRUE; + } + + return $this->userData[$user->uid]['tag_create'] == self::GRANT; + } + public function authTagDelete(object $user, VersioncontrolTag $tag) { + return $this->authLabel($user, $tag, 'delete'); + } + public function authTagUpdate(object $user, VersioncontrolTag $tag) { + return $this->authLabel($user, $tag, 'update'); + } + + protected function authLabel(object $user, VersioncontrolEntity $label, $op) { + $base = $this->baseAuth($user); + switch ($base) { + case self::DENY: + // Zero access, deny. + return FALSE; + case self::ALL: + // User has super cow powers, say yes. + return TRUE; + } + + $type = $label instanceof VersioncontrolTag ? 'tag' : 'branch'; + + switch ($this->userData[$user->uid][$type . '_' . $op]) { + case self::DENY: + // User has no perms for this op on this label type + return FALSE; + case self::ALL: + // User has all perms for this op on this label type + return TRUE; + } + + // If we get this far, then we're doing a label-specific perm check. + return $this->userData[$user->uid]['per-label-auth'][$label->label_id][$op] == self::GRANT; + } + + public function getErrorMessages() { + return $this->errors; + } + + public function setUserData($uid, $data) { + $this->userData[$uid] = $data; + } + + public function getUserData($uid = NULL) { + if (is_null($uid)) { + return $this->userData; + } + else { + return $this->userData[$uid]; + } + } + + public function save() { + if (!isset($this->repository)) { + throw new Exception('Cannot save auth data without a repository to attach to.', E_ERROR); + } + db_delete('versioncontrol_auth_account') + ->condition('repo_id', $this->repository->repo_id); + db_delete('versioncontrol_auth_account_label') + ->condition('repo_id', $this->repository->repo_id); + + // Prepare values + $base_values = array(); + $per_label_values = array(); + foreach ($this->userData as $uid => $data) { + $data['uid'] = $uid; + $data['repo_id'] = $this->repository->repo_id; + + foreach ($data['per-label-auth'] as $label_id => $label_data) { + $label_data['uid'] = $uid; + $label_data['repo_id'] = $this->repository->repo_id; + $label_data['label_id'] = $label_id; + + $per_label_values[] = $label_data; + } + + unset($data['per-label-auth']); + $base_values[] = $data; + } + + // Perform base insert + $fields = array('uid', 'repo_id', 'access', 'branch_create', + 'branch_update', 'branch_delete', 'tag_create', 'tag_update', + 'tag_delete'); + + $insert = db_insert('versioncontrol_auth_account')->fields($fields); + + foreach ($base_values as $record) { + $insert->values($record); + } + $insert->execute(); + + $fields = array('uid', 'repo_id', 'label_id', 'update', 'delete'); + $insert = db_insert('versioncontrol_auth_account_label')->fields($fields); + + foreach ($per_label_values as $record) { + $insert->values($record); + } + $insert->execute(); + } +} \ No newline at end of file diff --git includes/plugins/vcs_auth/account.inc includes/plugins/vcs_auth/account.inc new file mode 100644 index 0000000..bb01820 --- /dev/null +++ includes/plugins/vcs_auth/account.inc @@ -0,0 +1,8 @@ + t('Free For All (unrestricted write access)'), + 'handler' => array( + 'class' => 'VersioncontrolAuthHandlerMappedAccounts', + ), +); diff --git includes/plugins/vcs_auth/ffa.inc includes/plugins/vcs_auth/ffa.inc new file mode 100644 index 0000000..f31414b --- /dev/null +++ includes/plugins/vcs_auth/ffa.inc @@ -0,0 +1,8 @@ + t('Free For All (unrestricted write access)'), + 'handler' => array( + 'class' => 'VersioncontrolAuthHandlerFFA', + ), +); \ No newline at end of file diff --git versioncontrol.install versioncontrol.install index 7bf0329..3d4254f 100644 --- versioncontrol.install +++ versioncontrol.install @@ -402,6 +402,115 @@ function versioncontrol_schema() { 'primary key' => array('uid', 'repo_id'), ); + $schema['versioncontrol_auth_account'] = array( + 'description' => 'ACL table, used by the VersioncontrolAuthHandlerMappedAccounts family of plugins, that stores ACL data on a per-uid/per-repo basis.', + 'fields' => array( + 'uid' => array( + 'description' => 'Foreign key to {users}.uid; uniquely identifies a Drupal user to whom this ACL data applies.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'repo_id' => array( + 'description' => 'Foreign key to {versioncontrol_repositories}.repo_id; identifies the repository to which this ACL data applies.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'access' => array( + 'type' => 'int', + 'description' => 'Base, global access to the repository. 0 indicates no access (disabled/inactive account; acts as a global deny), 1 indicates some level of access, 2 indicates global access (overrides granular access).', + 'size' => 'tiny', + 'not null' => TRUE, + 'default' => 0, + ), + 'branch_create' => array( + 'type' => 'int', + 'description' => 'Grant user access to create branches in the repository.', + 'size' => 'tiny', + 'not null' => TRUE, + 'default' => 0, + ), + 'update_any_branch' => array( + 'type' => 'int', + 'description' => 'Grant user access to update/write to any branch in the repository. 1 is global access, 0 defers to individual branch perms.', + 'size' => 'tiny', + 'not null' => TRUE, + 'default' => 0, + ), + 'delete_any_branch' => array( + 'type' => 'int', + 'description' => 'Grant user access to delete any branch in the repository. 1 is global access, 0 defers to individual branch perms.', + 'size' => 'tiny', + 'not null' => TRUE, + 'default' => 0, + ), + 'tag_create' => array( + 'type' => 'int', + 'description' => 'Grant user access to create tags in the repository.', + 'size' => 'tiny', + 'not null' => TRUE, + 'default' => 0, + ), + 'update_any_tag' => array( + 'type' => 'int', + 'description' => 'Grant user access to update/modify any tag in the repository. 1 is global access, 0 defers to individual tag perms.', + 'size' => 'tiny', + 'not null' => TRUE, + 'default' => 0, + ), + 'delete_any_tag' => array( + 'type' => 'int', + 'description' => 'Grant user access to delete any tag in the repository. 1 is global access, 0 defers to individual tag perms.', + 'size' => 'tiny', + 'not null' => TRUE, + 'default' => 0, + ), + ), + 'primary key' => array('repo_id', 'uid'), + ); + $schema['versioncontrol_auth_account_label'] = array( + 'description' => '', + 'fields' => array( + 'uid' => array( + 'description' => 'Foreign key to {users}.uid; uniquely identifies a Drupal user to whom this ACL data applies.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'repo_id' => array( + 'description' => 'Foreign key to {versioncontrol_repositories}.repo_id; identifies the repository to which this ACL data applies.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'label_id' => array( + 'description' => 'Foreign key to {versioncontrol_labels}.label_id; identifies the label (branch or tag) to which this ACL data applies.', + 'type' => 'serial', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'update' => array( + 'type' => 'int', + 'description' => 'Grant user access to update/modify this label.', + 'size' => 'tiny', + 'not null' => TRUE, + 'default' => 0, + ), + 'delete' => array( + 'type' => 'int', + 'description' => 'Grant user access to delete this label.', + 'size' => 'tiny', + 'not null' => TRUE, + 'default' => 0, + ), + ), + 'primary key' => array('repo_id', 'uid', 'label_id'), + 'indexes' => array( + 'label_id' => array('label_id'), + ), + ); + return $schema; } @@ -857,3 +966,128 @@ function versioncontrol_update_6308() { db_add_field($ret, 'versioncontrol_repositories', 'plugins', $plugins); return $ret; } + +/** + * Remove the defunct versioncontrol_accounts table and replace it with ones + * driven by auth plugins, specifically the + * VersioncontrolAuthHandlerMappedAccounts family of plugins. + */ +function versioncontrol_update_6309() { + $ret = array(); + + $account_table = array( + 'description' => 'ACL table, used by the VersioncontrolAuthHandlerMappedAccounts family of plugins, that stores ACL data on a per-uid/per-repo basis.', + 'fields' => array( + 'uid' => array( + 'description' => 'Foreign key to {users}.uid; uniquely identifies a Drupal user to whom this ACL data applies.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'repo_id' => array( + 'description' => 'Foreign key to {versioncontrol_repositories}.repo_id; identifies the repository to which this ACL data applies.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'access' => array( + 'type' => 'int', + 'description' => 'Base, global access to the repository. 0 indicates no access (disabled/inactive account; acts as a global deny), 1 indicates some level of access, 2 indicates global access (overrides granular access).', + 'size' => 'tiny', + 'not null' => TRUE, + 'default' => 0, + ), + 'branch_create' => array( + 'type' => 'int', + 'description' => 'Grant user access to create branches in the repository.', + 'size' => 'tiny', + 'not null' => TRUE, + 'default' => 0, + ), + 'branch_update' => array( + 'type' => 'int', + 'description' => 'Grant user access to update/write to any branch in the repository. 1 is global access, 0 defers to individual branch perms.', + 'size' => 'tiny', + 'not null' => TRUE, + 'default' => 0, + ), + 'branch_delete' => array( + 'type' => 'int', + 'description' => 'Grant user access to delete any branch in the repository. 1 is global access, 0 defers to individual branch perms.', + 'size' => 'tiny', + 'not null' => TRUE, + 'default' => 0, + ), + 'tag_create' => array( + 'type' => 'int', + 'description' => 'Grant user access to create tags in the repository.', + 'size' => 'tiny', + 'not null' => TRUE, + 'default' => 0, + ), + 'tag_update' => array( + 'type' => 'int', + 'description' => 'Grant user access to update/modify any tag in the repository. 1 is global access, 0 defers to individual tag perms.', + 'size' => 'tiny', + 'not null' => TRUE, + 'default' => 0, + ), + 'tag_delete' => array( + 'type' => 'int', + 'description' => 'Grant user access to delete tags in the repository. 0 is no access, 1 is some access, 2 can delete all tags.', + 'size' => 'tiny', + 'not null' => TRUE, + 'default' => 0, + ), + ), + 'primary key' => array('repo_id', 'uid'), + ); + + db_create_table($ret, 'versioncontrol_auth_account', $account_table); + + $auth_label_table = array( + 'description' => '', + 'fields' => array( + 'uid' => array( + 'description' => 'Foreign key to {users}.uid; uniquely identifies a Drupal user to whom this ACL data applies.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'repo_id' => array( + 'description' => 'Foreign key to {versioncontrol_repositories}.repo_id; identifies the repository to which this ACL data applies.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'label_id' => array( + 'description' => 'Foreign key to {versioncontrol_labels}.label_id; identifies the label (branch or tag) to which this ACL data applies.', + 'type' => 'serial', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'update' => array( + 'type' => 'int', + 'description' => 'Grant user access to update/modify this label.', + 'size' => 'tiny', + 'not null' => TRUE, + 'default' => 0, + ), + 'delete' => array( + 'type' => 'int', + 'description' => 'Grant user access to delete this label.', + 'size' => 'tiny', + 'not null' => TRUE, + 'default' => 0, + ), + ), + 'primary key' => array('repo_id', 'uid', 'label_id'), + 'indexes' => array( + 'label_id' => array('label_id'), + ), + ); + + db_create_table($ret, 'versioncontrol_auth_account_label', $auth_label_table); + + return $ret; +} \ No newline at end of file