1
1
Fork 0
mirror of https://code.mensbeam.com/MensBeam/Arsse.git synced 2024-12-22 21:22:40 +00:00

Implement CLI for tokens

This commit is contained in:
J. King 2021-02-10 21:40:51 -05:00
parent 3795b1ccd8
commit fa6d641634
9 changed files with 212 additions and 48 deletions

View file

@ -6,6 +6,7 @@ New features:
- Support for API level 15 of Tiny Tiny RSS - Support for API level 15 of Tiny Tiny RSS
- Support for feed icons in Fever - Support for feed icons in Fever
- Command-line functionality for managing user metadata - Command-line functionality for managing user metadata
- Command-line functionality for managing Miniflux login tokens
Bug fixes: Bug fixes:
- Use icons specified in Atom feeds when available - Use icons specified in Atom feeds when available

View file

@ -8,6 +8,7 @@ namespace JKingWeb\Arsse;
use JKingWeb\Arsse\REST\Fever\User as Fever; use JKingWeb\Arsse\REST\Fever\User as Fever;
use JKingWeb\Arsse\ImportExport\OPML; use JKingWeb\Arsse\ImportExport\OPML;
use JKingWeb\Arsse\REST\Miniflux\Token as Miniflux;
class CLI { class CLI {
public const USAGE = <<<USAGE_TEXT public const USAGE = <<<USAGE_TEXT
@ -27,6 +28,9 @@ Usage:
arsse.php user unset-pass <username> arsse.php user unset-pass <username>
[--oldpass=<pass>] [--fever] [--oldpass=<pass>] [--fever]
arsse.php user auth <username> <password> [--fever] arsse.php user auth <username> <password> [--fever]
arsse.php token list <username>
arsse.php token create <username> [<label>]
arsse.php token revoke <username> [<token>]
arsse.php import <username> [<file>] arsse.php import <username> [<file>]
[-f | --flat] [-r | --replace] [-f | --flat] [-r | --replace]
arsse.php export <username> [<file>] arsse.php export <username> [<file>]
@ -125,6 +129,24 @@ Commands:
The --fever option may be used to test the user's Fever protocol password, The --fever option may be used to test the user's Fever protocol password,
if any. if any.
token list <username>
Lists available tokens for <username> in a simple tabular format. These
tokens act as an alternative means of authentication for the Miniflux
protocol and may be required by some clients. They do not expire.
token create <username> [<label>]
Creates a new login token for <username> and prints it. These tokens act
as an alternative means of authentication for the Miniflux protocol and
may be required by some clients. An optional label may be specified to
give the token a meaningful name.
token revoke <username> [<token>]
Deletes the specified token from the database. The token itself must be
supplied, not its label. If it is omitted all tokens are revoked.
import <username> [<file>] import <username> [<file>]
Imports the feeds, folders, and tags found in the OPML formatted <file> Imports the feeds, folders, and tags found in the OPML formatted <file>
@ -278,6 +300,15 @@ USAGE_TEXT;
$u = $args['<username>']; $u = $args['<username>'];
$file = $this->resolveFile($args['<file>'], "r"); $file = $this->resolveFile($args['<file>'], "r");
return (int) !Arsse::$obj->get(OPML::class)->importFile($file, $u, ($args['--flat'] || $args['-f']), ($args['--replace'] || $args['-r'])); return (int) !Arsse::$obj->get(OPML::class)->importFile($file, $u, ($args['--flat'] || $args['-f']), ($args['--replace'] || $args['-r']));
case "token list":
case "list token": // command reconstruction yields this order for "token list" command
return $this->tokenList($args['<username>']);
case "token create":
echo Arsse::$obj->get(Miniflux::class)->tokenGenerate($args['<username>'], $args['<label>']).\PHP_EOL;
return 0;
case "token revoke":
Arsse::$db->tokenRevoke($args['<username>'], "miniflux.login", $args['<token>']);
return 0;
case "user add": case "user add":
$out = $this->userAddOrSetPassword("add", $args["<username>"], $args["<password>"]); $out = $this->userAddOrSetPassword("add", $args["<username>"], $args["<password>"]);
if ($args['--admin']) { if ($args['--admin']) {
@ -315,6 +346,8 @@ USAGE_TEXT;
case "user list": case "user list":
case "user": case "user":
return $this->userList(); return $this->userList();
default:
throw new Exception("constantUnknown", $cmd); // @codeCoverageIgnore
} }
} catch (AbstractException $e) { } catch (AbstractException $e) {
$this->logError($e->getMessage()); $this->logError($e->getMessage());
@ -365,4 +398,15 @@ USAGE_TEXT;
} }
return 0; return 0;
} }
protected function tokenList(string $user): int {
$list = Arsse::$obj->get(Miniflux::class)->tokenList($user);
usort($list, function($v1, $v2) {
return $v1['label'] <=> $v2['label'];
});
foreach ($list as $t) {
echo $t['id']." ".$t['label'].\PHP_EOL;
}
return 0;
}
} }

View file

@ -0,0 +1,31 @@
<?php
/** @license MIT
* Copyright 2017 J. King, Dustin Wilson et al.
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\REST\Miniflux;
use JKingWeb\Arsse\Arsse;
use JKingWeb\Arsse\User\ExceptionConflict;
class Token {
protected const TOKEN_LENGTH = 32;
public function tokenGenerate(string $user, ?string $label = null): string {
// Miniflux produces tokens in base64url alphabet
$t = str_replace(["+", "/"], ["-", "_"], base64_encode(random_bytes(self::TOKEN_LENGTH)));
return Arsse::$db->tokenCreate($user, "miniflux.login", $t, null, $label);
}
public function tokenList(string $user): array {
if (!Arsse::$db->userExists($user)) {
throw new ExceptionConflict("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
}
$out = [];
foreach (Arsse::$db->tokenList($user, "miniflux.login") as $r) {
$out[] = ['label' => $r['data'], 'id' => $r['id']];
}
return $out;
}
}

View file

@ -34,7 +34,6 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler {
protected const ACCEPTED_TYPES_OPML = ["application/xml", "text/xml", "text/x-opml"]; protected const ACCEPTED_TYPES_OPML = ["application/xml", "text/xml", "text/x-opml"];
protected const ACCEPTED_TYPES_JSON = ["application/json"]; protected const ACCEPTED_TYPES_JSON = ["application/json"];
protected const TOKEN_LENGTH = 32;
protected const DEFAULT_ENTRY_LIMIT = 100; protected const DEFAULT_ENTRY_LIMIT = 100;
protected const DEFAULT_ORDER_COL = "modified_date"; protected const DEFAULT_ORDER_COL = "modified_date";
protected const DATE_FORMAT_SEC = "Y-m-d\TH:i:sP"; protected const DATE_FORMAT_SEC = "Y-m-d\TH:i:sP";
@ -1201,21 +1200,4 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler {
protected function opmlExport(): ResponseInterface { protected function opmlExport(): ResponseInterface {
return new GenericResponse(Arsse::$obj->get(OPML::class)->export(Arsse::$user->id), 200, ['Content-Type' => "application/xml"]); return new GenericResponse(Arsse::$obj->get(OPML::class)->export(Arsse::$user->id), 200, ['Content-Type' => "application/xml"]);
} }
public static function tokenGenerate(string $user, string $label): string {
// Miniflux produces tokens in base64url alphabet
$t = str_replace(["+", "/"], ["-", "_"], base64_encode(random_bytes(self::TOKEN_LENGTH)));
return Arsse::$db->tokenCreate($user, "miniflux.login", $t, null, $label);
}
public static function tokenList(string $user): array {
if (!Arsse::$db->userExists($user)) {
throw new ExceptionConflict("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
}
$out = [];
foreach (Arsse::$db->tokenList($user, "miniflux.login") as $r) {
$out[] = ['label' => $r['data'], 'id' => $r['id']];
}
return $out;
}
} }

View file

@ -14,6 +14,7 @@ use JKingWeb\Arsse\Database;
use JKingWeb\Arsse\Service; use JKingWeb\Arsse\Service;
use JKingWeb\Arsse\CLI; use JKingWeb\Arsse\CLI;
use JKingWeb\Arsse\REST\Fever\User as FeverUser; use JKingWeb\Arsse\REST\Fever\User as FeverUser;
use JKingWeb\Arsse\REST\Miniflux\Token as MinifluxToken;
use JKingWeb\Arsse\ImportExport\OPML; use JKingWeb\Arsse\ImportExport\OPML;
/** @covers \JKingWeb\Arsse\CLI */ /** @covers \JKingWeb\Arsse\CLI */
@ -418,4 +419,54 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest {
["arsse.php user unset john bogus", "john", ['bogus' => null], [], 1], ["arsse.php user unset john bogus", "john", ['bogus' => null], [], 1],
]; ];
} }
public function testListTokens(): void {
$data = [
['label' => 'Ook', 'id' => "TOKEN 1"],
['label' => 'Eek', 'id' => "TOKEN 2"],
['label' => null, 'id' => "TOKEN 3"],
['label' => 'Ack', 'id' => "TOKEN 4"],
];
$exp = implode(\PHP_EOL, [
"TOKEN 3 ",
"TOKEN 4 Ack",
"TOKEN 2 Eek",
"TOKEN 1 Ook",
]);
$t = \Phake::mock(MinifluxToken::class);
\Phake::when(Arsse::$obj)->get(MinifluxToken::class)->thenReturn($t);
\Phake::when($t)->tokenList->thenReturn($data);
$this->assertConsole($this->cli, "arsse.php token list john", 0, $exp);
\Phake::verify($t)->tokenList("john");
}
public function testCreateToken(): void {
$t = \Phake::mock(MinifluxToken::class);
\Phake::when(Arsse::$obj)->get(MinifluxToken::class)->thenReturn($t);
\Phake::when($t)->tokenGenerate->thenReturn("RANDOM TOKEN");
$this->assertConsole($this->cli, "arse.php token create jane", 0, "RANDOM TOKEN");
\Phake::verify($t)->tokenGenerate("jane", null);
}
public function testCreateTokenWithLabel(): void {
$t = \Phake::mock(MinifluxToken::class);
\Phake::when(Arsse::$obj)->get(MinifluxToken::class)->thenReturn($t);
\Phake::when($t)->tokenGenerate->thenReturn("RANDOM TOKEN");
$this->assertConsole($this->cli, "arse.php token create jane Ook", 0, "RANDOM TOKEN");
\Phake::verify($t)->tokenGenerate("jane", "Ook");
}
public function testRevokeAToken(): void {
Arsse::$db = \Phake::mock(Database::class);
\Phake::when(Arsse::$db)->tokenRevoke->thenReturn(true);
$this->assertConsole($this->cli, "arse.php token revoke jane TOKEN_ID", 0);
\Phake::verify(Arsse::$db)->tokenRevoke("jane", "miniflux.login", "TOKEN_ID");
}
public function testRevokeAllTokens(): void {
Arsse::$db = \Phake::mock(Database::class);
\Phake::when(Arsse::$db)->tokenRevoke->thenReturn(true);
$this->assertConsole($this->cli, "arse.php token revoke jane", 0);
\Phake::verify(Arsse::$db)->tokenRevoke("jane", "miniflux.login", null);
}
} }

View file

@ -0,0 +1,13 @@
<?php
/** @license MIT
* Copyright 2017 J. King, Dustin Wilson et al.
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\TestCase\REST\Miniflux\PDO;
/** @covers \JKingWeb\Arsse\REST\Miniflux\Token<extended>
* @group optional */
class TestToken extends \JKingWeb\Arsse\TestCase\REST\Miniflux\TestV1 {
use \JKingWeb\Arsse\Test\PDOTest;
}

View file

@ -0,0 +1,70 @@
<?php
/** @license MIT
* Copyright 2017 J. King, Dustin Wilson et al.
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\TestCase\REST\Miniflux;
use JKingWeb\Arsse\Arsse;
use JKingWeb\Arsse\Database;
use JKingWeb\Arsse\Db\Transaction;
use JKingWeb\Arsse\REST\Miniflux\Token;
use JKingWeb\Arsse\Test\Result;
/** @covers \JKingWeb\Arsse\REST\Miniflux\Token<extended> */
class TestToken extends \JKingWeb\Arsse\Test\AbstractTest {
protected const NOW = "2020-12-09T22:35:10.023419Z";
protected const TOKEN = "Tk2o9YubmZIL2fm2w8Z4KlDEQJz532fNSOcTG0s2_xc=";
protected $h;
protected $transaction;
public function setUp(): void {
self::clearData();
self::setConf();
// create a mock database interface
Arsse::$db = \Phake::mock(Database::class);
$this->transaction = \Phake::mock(Transaction::class);
\Phake::when(Arsse::$db)->begin->thenReturn($this->transaction);
$this->h = new Token();
}
public function tearDown(): void {
self::clearData();
}
protected function v($value) {
return $value;
}
public function testGenerateTokens(): void {
\Phake::when(Arsse::$db)->tokenCreate->thenReturn("RANDOM TOKEN");
$this->assertSame("RANDOM TOKEN", $this->h->tokenGenerate("ook", "Eek"));
\Phake::verify(Arsse::$db)->tokenCreate("ook", "miniflux.login", \Phake::capture($token), null, "Eek");
$this->assertRegExp("/^[A-Za-z0-9_\-]{43}=$/", $token);
}
public function testListTheTokensOfAUser(): void {
$out = [
['id' => "TOKEN 1", 'data' => "Ook"],
['id' => "TOKEN 2", 'data' => "Eek"],
['id' => "TOKEN 3", 'data' => "Ack"],
];
$exp = [
['label' => "Ook", 'id' => "TOKEN 1"],
['label' => "Eek", 'id' => "TOKEN 2"],
['label' => "Ack", 'id' => "TOKEN 3"],
];
\Phake::when(Arsse::$db)->tokenList->thenReturn(new Result($this->v($out)));
\Phake::when(Arsse::$db)->userExists->thenReturn(true);
$this->assertSame($exp, $this->h->tokenList("john.doe@example.com"));
\Phake::verify(Arsse::$db)->tokenList("john.doe@example.com", "miniflux.login");
}
public function testListTheTokensOfAMissingUser(): void {
\Phake::when(Arsse::$db)->userExists->thenReturn(false);
$this->assertException("doesNotExist", "User", "ExceptionConflict");
$this->h->tokenList("john.doe@example.com");
}
}

View file

@ -978,34 +978,4 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest {
$this->assertMessage(new TextResponse("EXPORT DATA", 200, ['Content-Type' => "application/xml"]), $this->req("GET", "/export")); $this->assertMessage(new TextResponse("EXPORT DATA", 200, ['Content-Type' => "application/xml"]), $this->req("GET", "/export"));
\Phake::verify($opml)->export(Arsse::$user->id); \Phake::verify($opml)->export(Arsse::$user->id);
} }
public function testGenerateTokens(): void {
\Phake::when(Arsse::$db)->tokenCreate->thenReturn("RANDOM TOKEN");
$this->assertSame("RANDOM TOKEN", V1::tokenGenerate("ook", "Eek"));
\Phake::verify(Arsse::$db)->tokenCreate("ook", "miniflux.login", \Phake::capture($token), null, "Eek");
$this->assertRegExp("/^[A-Za-z0-9_\-]{43}=$/", $token);
}
public function testListTheTokensOfAUser(): void {
$out = [
['id' => "TOKEN 1", 'data' => "Ook"],
['id' => "TOKEN 2", 'data' => "Eek"],
['id' => "TOKEN 3", 'data' => "Ack"],
];
$exp = [
['label' => "Ook", 'id' => "TOKEN 1"],
['label' => "Eek", 'id' => "TOKEN 2"],
['label' => "Ack", 'id' => "TOKEN 3"],
];
\Phake::when(Arsse::$db)->tokenList->thenReturn(new Result($this->v($out)));
\Phake::when(Arsse::$db)->userExists->thenReturn(true);
$this->assertSame($exp, V1::tokenList("john.doe@example.com"));
\Phake::verify(Arsse::$db)->tokenList("john.doe@example.com", "miniflux.login");
}
public function testListTheTokensOfAMissingUser(): void {
\Phake::when(Arsse::$db)->userExists->thenReturn(false);
$this->assertException("doesNotExist", "User", "ExceptionConflict");
V1::tokenList("john.doe@example.com");
}
} }

View file

@ -118,7 +118,9 @@
<file>cases/REST/Miniflux/TestErrorResponse.php</file> <file>cases/REST/Miniflux/TestErrorResponse.php</file>
<file>cases/REST/Miniflux/TestStatus.php</file> <file>cases/REST/Miniflux/TestStatus.php</file>
<file>cases/REST/Miniflux/TestV1.php</file> <file>cases/REST/Miniflux/TestV1.php</file>
<file>cases/REST/Miniflux/TestToken.php</file>
<file>cases/REST/Miniflux/PDO/TestV1.php</file> <file>cases/REST/Miniflux/PDO/TestV1.php</file>
<file>cases/REST/Miniflux/PDO/TestToken.php</file>
</testsuite> </testsuite>
<testsuite name="NCNv1"> <testsuite name="NCNv1">
<file>cases/REST/NextcloudNews/TestVersions.php</file> <file>cases/REST/NextcloudNews/TestVersions.php</file>