Skip to content

Commit

Permalink
Merge pull request #76 from akirk/add-scope-adherence
Browse files Browse the repository at this point in the history
Add scope adherence
  • Loading branch information
akirk committed Feb 14, 2024
2 parents 9998ea1 + 90d63c9 commit dfb6e36
Show file tree
Hide file tree
Showing 9 changed files with 283 additions and 90 deletions.
137 changes: 89 additions & 48 deletions includes/class-mastodon-api.php

Large diffs are not rendered by default.

21 changes: 4 additions & 17 deletions includes/class-mastodon-app.php
Expand Up @@ -118,23 +118,6 @@ public function check_redirect_uri( $redirect_uri ) {
return false;
}

public function check_scopes( $requested_scopes ) {
$allowed_scopes = explode( ' ', $this->get_scopes() );

foreach ( explode( ' ', $requested_scopes ) as $s ) {
if ( false !== strpos( $s, ':' ) ) {
list( $scope, $subscope ) = explode( ':', $s, 2 );
} else {
$scope = $s;
}
if ( ! in_array( $scope, $allowed_scopes, true ) ) {
return false;
}
}

return false;
}

public function delete_last_requests() {
return delete_metadata( 'term', $this->term->term_id, 'request' );
}
Expand Down Expand Up @@ -512,6 +495,10 @@ function ( $post_format ) {
return $args;
}

public function has_scope( $requested_scope ) {
return OAuth2\Scope_Util::checkSingleScope( $requested_scope, $this->get_scopes() );
}

/**
* Get an app via client_id.
*
Expand Down
1 change: 1 addition & 0 deletions includes/class-mastodon-oauth.php
Expand Up @@ -44,6 +44,7 @@ public function __construct() {
$this->server = new Server( new Oauth2\Authorization_Code_Storage(), $config );
$this->server->addStorage( new Oauth2\Mastodon_App_Storage(), 'client_credentials' );
$this->server->addStorage( new Oauth2\Access_Token_Storage(), 'access_token' );
$this->server->setScopeUtil( new Oauth2\Scope_Util() );

if ( '/oauth/token' === strtok( $_SERVER['REQUEST_URI'], '?' ) ) {
// Avoid interference with private site plugins.
Expand Down
10 changes: 2 additions & 8 deletions includes/oauth2/class-access-token-storage.php
Expand Up @@ -167,14 +167,8 @@ public function getAccessToken( $oauth_token ) {
$access_token = array(
'access_token' => $oauth_token,
);
foreach ( array(
'client_id' => 'client_id',
'user_id' => 'user_id',
'expires' => 'expires',
'redirect_uri' => 'redirect_uri',
'scope' => 'scope',
) as $key => $meta_key ) {
$access_token[ $key ] = get_term_meta( $term->term_id, $meta_key, true );
foreach ( array_keys( self::$access_token_data ) as $meta_key ) {
$access_token[ $meta_key ] = get_term_meta( $term->term_id, $meta_key, true );
}

$access_token['created_at'] = $access_token['expires'] - YEAR_IN_SECONDS * 2;
Expand Down
91 changes: 74 additions & 17 deletions includes/oauth2/class-authenticate-handler.php
Expand Up @@ -35,14 +35,21 @@ public function handle( Request $request, Response $response ) {
return $redirect_uri;
}

$scopes = $app->check_scopes( $_GET['scope'] );
if ( is_wp_error( $scopes ) ) {
return $scopes;
$scopes = array();
foreach ( explode( ' ', $_GET['scope'] ) as $scope ) {
if ( $app->has_scope( $scope ) ) {
$scopes[] = $scope;
}
}
if ( empty( $scopes ) ) {
$response->setError( 403, 'invalid_scopes', 'Invalid scope was requested.' );
return $response;
}

$data = array(
'user' => wp_get_current_user(),
'client_name' => $client_name,
'scopes' => implode( ' ', $scopes ),
'body_class_attr' => implode( ' ', array_diff( get_body_class(), array( 'error404' ) ) ),
'cancel_url' => $this->get_cancel_url( $request ),
'form_url' => home_url( '/oauth/authorize' ),
Expand Down Expand Up @@ -96,6 +103,27 @@ private function render_no_permission_screen( $data ) {
}

private function render_consent_screen( $data ) {
$scope_explanations = array(
'read' => __( 'Read information from your account, for example read your statuses.', 'enable-mastodon-apps' ),
'write' => __( 'Write information to your account, for example post a status on your behalf.', 'enable-mastodon-apps' ),
'follow' => __( 'Follow other accounts using your account.', 'enable-mastodon-apps' ),
'push' => __( 'Subscribe to push events for your account.', 'enable-mastodon-apps' ),
);

$requested_scopes = array();
foreach ( explode( ' ', $data['scopes'] ) as $scope ) {
$p = strpos( $scope, ':' );
if ( false === $p ) {
$requested_scopes[ $scope ] = array( 'all' => true );
} else {
$main_scope = substr( $scope, 0, $p );
if ( ! isset( $requested_scopes[ $main_scope ] ) ) {
$requested_scopes[ $main_scope ] = array();
}
$requested_scopes[ $main_scope ][ substr( $scope, $p + 1 ) ] = true;
}
}

?>
<div id="openid-connect-authenticate">
<div id="openid-connect-authenticate-form-container" class="login">
Expand All @@ -113,23 +141,52 @@ private function render_consent_screen( $data ) {
</h2>
<br/>
<p>
<label>
<?php
echo wp_kses(
sprintf(
<?php
echo wp_kses(
sprintf(
// translators: %1$s is the site name, %2$s is the username.
__( 'Do you want to log in to <em>%1$s</em> with your <em>%2$s</em> account?', 'enable-mastodon-apps' ),
$data['client_name'],
get_bloginfo( 'name' )
),
array(
'em' => array(),
)
);
?>
</label>
__( 'Do you want to log in to <strong>%1$s</strong> with your <strong>%2$s</strong> account?', 'enable-mastodon-apps' ),
$data['client_name'],
get_bloginfo( 'name' )
),
array(
'strong' => array(),
)
);
?>
</p>
<br/>
<p>
<?php
esc_html_e( 'Requested permissions:', 'enable-mastodon-apps' );
?>
</p>
<ul style="margin-left: 1em">

<?php
foreach ( $requested_scopes as $main_scope => $subscopes ) {
if ( ! isset( $scope_explanations[ $main_scope ] ) ) {
continue;
}
echo '<li style="margin-top: .5em" title="', esc_attr( $main_scope ), '">', esc_html( $scope_explanations[ $main_scope ] );
$all = isset( $subscopes['all'] ) && $subscopes['all'];
unset( $subscopes['all'] );
if ( ! empty( $subscopes ) && ! $all ) {
echo ' ', __( 'Only the following sub-permissions:', 'enable-mastodon-apps' );
echo '<ul style="margin-left: 1em">';
foreach ( $subscopes as $subscope => $true ) {
if ( ! isset( $scope_explanations[ $main_scope . ':' . $subscope ] ) ) {
$scope_explanations[ $main_scope . ':' . $subscope ] = ucwords( $subscope ) . ' (' . $main_scope . ':' . $subscope . ')';
}
echo '<li style="margin-top: .5em" title="', esc_attr( $main_scope . ':' . $subscope ), '">', esc_html( $scope_explanations[ $main_scope . ':' . $subscope ] ), '</li>';
}
echo '</ul>';
}
echo '</li>';
}
?>
</ul>
<br/>
<?php wp_nonce_field( 'wp_rest' ); /* The nonce will give the REST call the userdata. */ ?>
<?php foreach ( $data['form_fields'] as $key => $value ) : ?>
<input type="hidden" name="<?php echo esc_attr( $key ); ?>" value="<?php echo esc_attr( $value ); ?>"/>
Expand Down
36 changes: 36 additions & 0 deletions includes/oauth2/class-scope-util.php
@@ -0,0 +1,36 @@
<?php
/**
* OAuth2 Scope Util
*
* @package Friends
*/

namespace Enable_Mastodon_Apps\OAuth2;

/**
* This class overrides the scope checking to allow for fine grained scopes.
*/
class Scope_Util extends \OAuth2\Scope {
public static function checkSingleScope( $required_scope, $available_scope ) {
$required_main_scope = strtok( $required_scope, ':' );
foreach ( explode( ' ', $available_scope ) as $scope ) {
if ( $scope === $required_scope ) {
return true;
}

if ( $scope === $required_main_scope ) {
return true;
}
}

return false;
}
public function checkScope( $required_scope, $available_scope ) {
foreach ( explode( ' ', $required_scope ) as $scope ) {
if ( ! self::checkSingleScope( $scope, $available_scope ) ) {
return false;
}
}
return true;
}
}
13 changes: 13 additions & 0 deletions tests/class-mastodon-api-testcase.php
Expand Up @@ -16,6 +16,7 @@ class Mastodon_API_TestCase extends \WP_UnitTestCase {
protected $token;
protected $post;
protected $friend_post;
protected $private_post;
protected $administrator;
protected $friend;
protected $app;
Expand Down Expand Up @@ -49,6 +50,18 @@ public function set_up() {
)
);
set_post_format( $this->post, 'status' );

$this->private_post = wp_insert_post(
array(
'post_author' => $this->administrator,
'post_content' => 'Private post',
'post_title' => '',
'post_status' => 'private',
'post_type' => 'post',
'post_date' => '2023-01-03 00:00:00',
)
);
set_post_format( $this->post, 'status' );
$args = array(
'supports' => array( 'title', 'editor', 'author', 'revisions', 'thumbnail', 'excerpt', 'comments', 'post-formats' ),
'taxonomies' => array( 'post_tag', 'post_format' ),
Expand Down
52 changes: 52 additions & 0 deletions tests/test-mastodon-app.php
@@ -0,0 +1,52 @@
<?php
/**
* Class Test_Apps_Endpoint
*
* @package Enable_Mastodon_Apps
*/

namespace Enable_Mastodon_Apps;

/**
* A testcase for the apps endpoint.
*
* @package
*/
class MastodonApp_Test extends \WP_UnitTestCase {
public function test_create_app() {
$app = Mastodon_App::save( 'test', array( Mastodon_OAuth::OOB_REDIRECT_URI ), 'read', '' );
$this->assertInstanceOf( Mastodon_App::class, $app );
}

public function test_create_app_with_empty_scope() {
$this->expectException( \Exception::class );
$app = Mastodon_App::save( 'test', array( Mastodon_OAuth::OOB_REDIRECT_URI ), '', '' );
}

/**
* Scopes to test
*
* @param string $app_scopes The application scopes.
* @param string $scope_to_test The scope to test.
* @param bool $has_scope Indicates if the test should assume the scope to be existent.
* @dataProvider scopes
*/
public function test_scope_given( $app_scopes, $scope_to_test, $has_scope ) {
$app = Mastodon_App::save( 'test', array( Mastodon_OAuth::OOB_REDIRECT_URI ), $app_scopes, '' );
$this->assertEquals( $has_scope, $app->has_scope( $scope_to_test ) );
}

public function scopes() {
return array(
array( 'read', 'read', true ),
array( 'read', 'read:accounts', true ),
array( 'read:accounts', 'read:accounts', true ),
array( 'read:accounts', 'read', false ),
array( 'write', 'read', false ),
array( 'read', 'write', false ),
array( 'read write', 'write', true ),
array( 'read write push', 'write', true ),
array( 'read', 'write:accounts', false ),
);
}
}
12 changes: 12 additions & 0 deletions tests/test-statuses-endpoint.php
Expand Up @@ -47,6 +47,18 @@ public function test_statuses_id() {
$this->assertIsInt( $data['favourites_count'] );
}

public function test_statuses_private_id() {
global $wp_rest_server;

$request = new \WP_REST_Request( 'GET', '/' . Mastodon_API::PREFIX . '/api/v1/statuses/' . $this->private_post );
$response = $wp_rest_server->dispatch( $request );
$this->assertEquals( 401, $response->get_status() );

$_SERVER['HTTP_AUTHORIZATION'] = 'Bearer ' . $this->token;
$response = $wp_rest_server->dispatch( $request );
$this->assertEquals( 200, $response->get_status() );
}

public function test_statuses_delete() {
global $wp_rest_server;
$request = new \WP_REST_Request( 'DELETE', '/' . Mastodon_API::PREFIX . '/api/v1/statuses/' . $this->post );
Expand Down

0 comments on commit dfb6e36

Please sign in to comment.