Browse Source

Implement CLI for tokens

rpm
J. King 3 years ago
parent
commit
fa6d641634
  1. 1
      CHANGELOG
  2. 44
      lib/CLI.php
  3. 31
      lib/REST/Miniflux/Token.php
  4. 18
      lib/REST/Miniflux/V1.php
  5. 51
      tests/cases/CLI/TestCLI.php
  6. 13
      tests/cases/REST/Miniflux/PDO/TestToken.php
  7. 70
      tests/cases/REST/Miniflux/TestToken.php
  8. 30
      tests/cases/REST/Miniflux/TestV1.php
  9. 2
      tests/phpunit.dist.xml

1
CHANGELOG

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

44
lib/CLI.php

@ -8,6 +8,7 @@ namespace JKingWeb\Arsse;
use JKingWeb\Arsse\REST\Fever\User as Fever;
use JKingWeb\Arsse\ImportExport\OPML;
use JKingWeb\Arsse\REST\Miniflux\Token as Miniflux;
class CLI {
public const USAGE = <<<USAGE_TEXT
@ -27,6 +28,9 @@ Usage:
arsse.php user unset-pass <username>
[--oldpass=<pass>] [--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>]
[-f | --flat] [-r | --replace]
arsse.php export <username> [<file>]
@ -125,6 +129,24 @@ Commands:
The --fever option may be used to test the user's Fever protocol password,
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>]
Imports the feeds, folders, and tags found in the OPML formatted <file>
@ -278,6 +300,15 @@ USAGE_TEXT;
$u = $args['<username>'];
$file = $this->resolveFile($args['<file>'], "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":
$out = $this->userAddOrSetPassword("add", $args["<username>"], $args["<password>"]);
if ($args['--admin']) {
@ -315,6 +346,8 @@ USAGE_TEXT;
case "user list":
case "user":
return $this->userList();
default:
throw new Exception("constantUnknown", $cmd); // @codeCoverageIgnore
}
} catch (AbstractException $e) {
$this->logError($e->getMessage());
@ -365,4 +398,15 @@ USAGE_TEXT;
}
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;
}
}

31
lib/REST/Miniflux/Token.php

@ -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;
}
}

18
lib/REST/Miniflux/V1.php

@ -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_JSON = ["application/json"];
protected const TOKEN_LENGTH = 32;
protected const DEFAULT_ENTRY_LIMIT = 100;
protected const DEFAULT_ORDER_COL = "modified_date";
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 {
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;
}
}

51
tests/cases/CLI/TestCLI.php

@ -14,6 +14,7 @@ use JKingWeb\Arsse\Database;
use JKingWeb\Arsse\Service;
use JKingWeb\Arsse\CLI;
use JKingWeb\Arsse\REST\Fever\User as FeverUser;
use JKingWeb\Arsse\REST\Miniflux\Token as MinifluxToken;
use JKingWeb\Arsse\ImportExport\OPML;
/** @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],
];
}
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);
}
}

13
tests/cases/REST/Miniflux/PDO/TestToken.php

@ -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;
}

70
tests/cases/REST/Miniflux/TestToken.php

@ -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");
}
}

30
tests/cases/REST/Miniflux/TestV1.php

@ -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"));
\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");
}
}

2
tests/phpunit.dist.xml

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

Loading…
Cancel
Save