perlsky is a Perl 5 implementation of an AT Protocol Personal Data Server.
13
fork

Configure Feed

Select the types of activity you want to include in your feed.

Tighten admin auth and make email auto-confirm explicit

alice 5773ea4b 484cf0f1

+97 -35
+4 -14
lib/ATProto/PDS/API/Helpers.pm
··· 7 7 8 8 use Exporter 'import'; 9 9 use JSON::PP (); 10 - use MIME::Base64 qw(decode_base64); 11 10 12 11 use ATProto::PDS::API::Util qw(iso8601 xrpc_error); 13 12 use ATProto::PDS::Auth::Password qw(verify_password); 14 - use ATProto::PDS::Moderation qw(subject_key); 13 + use ATProto::PDS::Moderation qw(admin_authorization_status subject_key); 15 14 16 15 our @EXPORT_OK = qw( 17 16 account_view ··· 28 27 xrpc_error(503, 'AdminAuthUnavailable', 'Admin password is not configured') 29 28 unless defined $configured && length $configured; 30 29 31 - my $auth = $c->req->headers->authorization // q(); 32 - my $provided; 33 - if ($auth =~ /\ABearer\s+(.+)\z/i) { 34 - $provided = $1; 35 - } elsif ($auth =~ /\ABasic\s+(.+)\z/i) { 36 - my $decoded = decode_base64($1); 37 - my (undef, $password) = split /:/, $decoded, 2; 38 - $provided = $password; 39 - } 40 - 30 + my ($valid, $supplied) = admin_authorization_status($c); 41 31 xrpc_error(401, 'AuthRequired', 'Admin authorization is required') 42 - unless defined $provided && length $provided; 32 + unless $supplied; 43 33 xrpc_error(403, 'InvalidAdminToken', 'Invalid admin authorization') 44 - unless $provided eq $configured; 34 + unless $valid; 45 35 return 1; 46 36 } 47 37
+8 -1
lib/ATProto/PDS/API/Server.pm
··· 103 103 did => $did, 104 104 handle => $handle, 105 105 email => $body->{email}, 106 - email_confirmed_at => $body->{email} ? time : undef, 106 + email_confirmed_at => _initial_email_confirmed_at($c, $body->{email}), 107 107 password_hash => $password_record->{hash}, 108 108 password_salt => $password_record->{salt}, 109 109 did_doc => $did_doc, ··· 779 779 sub _uses_admin_authorization ($c) { 780 780 my $auth = $c->req->headers->authorization // q(); 781 781 return 1 if $auth =~ /\ABasic\s+/i; 782 + return 0 unless $c->config_value('legacy_admin_bearer_auth', 0); 782 783 return 0 unless $auth =~ /\ABearer\s+(\S+)\z/i; 783 784 my $token = $1; 784 785 return $token !~ /\A[^.]+\.[^.]+\.[^.]+\z/; 786 + } 787 + 788 + sub _initial_email_confirmed_at ($c, $email) { 789 + return undef unless defined $email && length $email; 790 + return undef unless $c->config_value('testing_auto_confirm_email', 1); 791 + return time; 785 792 } 786 793 787 794 sub _require_action_token ($c, %args) {
+28 -2
lib/ATProto/PDS/Moderation.pm
··· 7 7 8 8 use Exporter 'import'; 9 9 use JSON::PP (); 10 + use MIME::Base64 qw(decode_base64); 10 11 11 12 use ATProto::PDS::API::Util qw(xrpc_error); 12 13 use ATProto::PDS::Auth::JWT qw(decode_jwt); 13 14 14 15 our @EXPORT_OK = qw( 16 + admin_authorization_status 15 17 assert_blob_readable 16 18 assert_login_allowed 17 19 assert_record_readable ··· 129 131 return 1; 130 132 } 131 133 134 + sub admin_authorization_status ($c, $auth = undef) { 135 + my $configured = $c->config_value('admin_password'); 136 + return (0, 0) unless defined $configured && length $configured; 137 + 138 + $auth //= $c->req->headers->authorization // q(); 139 + return (0, 0) unless defined $auth && length $auth; 140 + 141 + if ($auth =~ /\ABearer\s+(.+)\z/i) { 142 + return (0, 1) unless $c->config_value('legacy_admin_bearer_auth', 0); 143 + return (($1 eq $configured) ? 1 : 0, 1); 144 + } 145 + 146 + if ($auth =~ /\ABasic\s+(.+)\z/i) { 147 + my $decoded = decode_base64($1); 148 + my ($username, $password) = split /:/, $decoded, 2; 149 + return ( 150 + (defined($username) && defined($password) && $username eq 'admin' && $password eq $configured) ? 1 : 0, 151 + 1, 152 + ); 153 + } 154 + 155 + return (0, 1); 156 + } 157 + 132 158 sub can_read_private_blob ($c, $did) { 133 159 my $auth = $c->req->headers->authorization // q(); 134 - return 1 if defined($c->config_value('admin_password')) && length($c->config_value('admin_password')) 135 - && $auth =~ /\ABearer\s+\Q@{[$c->config_value('admin_password')]}\E\z/; 160 + my ($admin_ok) = admin_authorization_status($c, $auth); 161 + return 1 if $admin_ok; 136 162 return 0 unless $auth =~ /\ABearer\s+(.+)\z/i; 137 163 my $token = $1; 138 164 my $decoded = eval { decode_jwt($token, $c->config_value('jwt_secret', 'perlsky-dev-secret')) };
+8 -3
t/email-confirmation.t
··· 30 30 service_handle_domain => 'example.test', 31 31 service_did_method => 'did:web', 32 32 jwt_secret => 'email-confirm-secret', 33 + testing_auto_confirm_email => 0, 33 34 data_dir => $tmp, 34 35 db_path => File::Spec->catfile($tmp, 'perlsky.sqlite'), 35 36 }, ··· 44 45 })->status_is(200); 45 46 my $alice = $t->tx->res->json; 46 47 47 - $app->store->update_account($alice->{did}, email_confirmed_at => undef); 48 + ok(!$alice->{emailConfirmed}, 'new account email stays unconfirmed when testing auto-confirm is disabled'); 49 + 50 + $t->post_ok('/xrpc/com.atproto.server.requestEmailUpdate' => { 51 + Authorization => "Bearer $alice->{accessJwt}", 52 + } => json => {})->status_is(200); 53 + ok(!$t->tx->res->json->{tokenRequired}, 'unconfirmed email does not require an update token'); 48 54 49 55 $t->post_ok('/xrpc/com.atproto.server.requestEmailConfirmation' => { 50 56 Authorization => "Bearer $alice->{accessJwt}", ··· 58 64 59 65 $app->store->update_account( 60 66 $alice->{did}, 61 - email => 'alice+new@example.test', 62 - email_confirmed_at => undef, 67 + email => 'alice+new@example.test', 63 68 ); 64 69 65 70 $t->post_ok('/xrpc/com.atproto.server.confirmEmail' => json => {
+5 -3
t/extended-api.t
··· 34 34 jwt_secret => 'extended-secret', 35 35 admin_password => 'admin-secret', 36 36 self_service_invite_codes => 1, 37 + testing_auto_confirm_email => 1, 37 38 data_dir => $tmp, 38 39 db_path => File::Spec->catfile($tmp, 'perlsky.sqlite'), 39 40 }, 40 41 ); 41 42 42 43 my $t = Test::Mojo->new($app); 44 + my $admin_auth = 'Basic YWRtaW46YWRtaW4tc2VjcmV0'; 43 45 44 46 $t->post_ok('/xrpc/com.atproto.server.createAccount' => json => { 45 47 handle => 'alice.example.test', ··· 99 101 ->json_is('/codes/0/code', $invite_code); 100 102 101 103 $t->get_ok('/xrpc/com.atproto.admin.getAccountInfo' => { 102 - Authorization => 'Bearer admin-secret', 104 + Authorization => $admin_auth, 103 105 } => form => { 104 106 did => $did, 105 107 })->status_is(200) ··· 107 109 ->json_is('/handle', 'alice.example.test'); 108 110 109 111 $t->post_ok('/xrpc/com.atproto.temp.addReservedHandle' => { 110 - Authorization => 'Bearer admin-secret', 112 + Authorization => $admin_auth, 111 113 } => json => { 112 114 handle => 'reserved.example.test', 113 115 })->status_is(200); ··· 197 199 like($t->tx->res->headers->content_type // '', qr{application/vnd\.ipld\.car}, 'block export is a CAR'); 198 200 199 201 $t->post_ok('/xrpc/com.atproto.admin.updateSubjectStatus' => { 200 - Authorization => 'Bearer admin-secret', 202 + Authorization => $admin_auth, 201 203 } => json => { 202 204 subject => { did => $did }, 203 205 takedown => { applied => JSON::PP::true, ref => 'unit-test' },
+3 -2
t/external-surface.t
··· 39 39 ); 40 40 41 41 my $t = Test::Mojo->new($app); 42 + my $admin_auth = 'Basic YWRtaW46YWRtaW4tc2VjcmV0'; 42 43 43 44 for my $endpoint (@{ $app->endpoint_catalog }) { 44 45 ok($app->api_registry->handler_for($endpoint->{id}), "$endpoint->{id} has a handler"); ··· 175 176 $t->get_ok(Mojo::URL->new('/xrpc/com.atproto.admin.getAccountInfo')->query( 176 177 did => $did, 177 178 ) => { 178 - Authorization => 'Bearer admin-secret', 179 + Authorization => $admin_auth, 179 180 })->status_is(200) 180 181 ->json_is('/handle' => 'alice.example.test'); 181 182 182 183 $t->post_ok('/xrpc/com.atproto.admin.updateSubjectStatus' => { 183 - Authorization => 'Bearer admin-secret', 184 + Authorization => $admin_auth, 184 185 } => json => { 185 186 subject => { uri => $record_uri, cid => $record_cid }, 186 187 takedown => { applied => JSON::PP::true },
+6 -5
t/labels.t
··· 43 43 my $service_did = service_did($app->settings); 44 44 my $t = Test::Mojo->new($app); 45 45 my $ws = Test::Mojo->new($app); 46 + my $admin_auth = 'Basic YWRtaW46YWRtaW4tc2VjcmV0'; 46 47 47 48 $t->post_ok('/xrpc/com.atproto.server.createAccount' => json => { 48 49 handle => 'alice.example.test', ··· 56 57 is($ws->message, undef, 'label stream is quiet without a backlog'); 57 58 58 59 $t->post_ok('/xrpc/com.atproto.admin.updateSubjectStatus' => { 59 - Authorization => 'Bearer admin-secret', 60 + Authorization => $admin_auth, 60 61 } => json => { 61 62 subject => { did => $did }, 62 63 takedown => { applied => JSON::PP::true }, ··· 97 98 my $bob_did = $t->tx->res->json->{did}; 98 99 99 100 $t->post_ok('/xrpc/com.atproto.admin.updateSubjectStatus' => { 100 - Authorization => 'Bearer admin-secret', 101 + Authorization => $admin_auth, 101 102 } => json => { 102 103 subject => { did => $bob_did }, 103 104 takedown => { applied => JSON::PP::true }, ··· 135 136 ); 136 137 137 138 $t->post_ok('/xrpc/com.atproto.admin.updateSubjectStatus' => { 138 - Authorization => 'Bearer admin-secret', 139 + Authorization => $admin_auth, 139 140 } => json => { 140 141 subject => { did => $did }, 141 142 takedown => { applied => JSON::PP::false }, ··· 167 168 ); 168 169 169 170 $t->post_ok('/xrpc/com.atproto.admin.updateSubjectStatus' => { 170 - Authorization => 'Bearer admin-secret', 171 + Authorization => $admin_auth, 171 172 } => json => { 172 173 subject => { did => $bob_did }, 173 174 takedown => { applied => JSON::PP::false }, ··· 194 195 $app->store->dbh->do(q{DELETE FROM events WHERE seq <= ?}, undef, $app->store->latest_event_seq); 195 196 196 197 $t->post_ok('/xrpc/com.atproto.admin.updateSubjectStatus' => { 197 - Authorization => 'Bearer admin-secret', 198 + Authorization => $admin_auth, 198 199 } => json => { 199 200 subject => { did => $did }, 200 201 takedown => { applied => JSON::PP::true },
+6 -5
t/moderation.t
··· 40 40 ); 41 41 42 42 my $t = Test::Mojo->new($app); 43 + my $admin_auth = 'Basic YWRtaW46YWRtaW4tc2VjcmV0'; 43 44 44 45 $t->post_ok('/xrpc/com.atproto.server.createAccount' => json => { 45 46 handle => 'alice.example.test', ··· 68 69 my $record_cid = $t->tx->res->json->{cid}; 69 70 70 71 $t->post_ok('/xrpc/com.atproto.admin.updateSubjectStatus' => { 71 - Authorization => 'Bearer admin-secret', 72 + Authorization => $admin_auth, 72 73 } => json => { 73 74 subject => { uri => "at://$did/app.bsky.feed.post/visible-post", cid => $record_cid }, 74 75 takedown => { applied => JSON::PP::true }, ··· 98 99 ); 99 100 100 101 $t->post_ok('/xrpc/com.atproto.admin.updateSubjectStatus' => { 101 - Authorization => 'Bearer admin-secret', 102 + Authorization => $admin_auth, 102 103 } => json => { 103 104 subject => { did => $did }, 104 105 takedown => { applied => JSON::PP::true }, ··· 158 159 ->json_is('/reasonType', 'com.atproto.moderation.defs#reasonAppeal'); 159 160 160 161 $t->post_ok('/xrpc/com.atproto.admin.updateSubjectStatus' => { 161 - Authorization => 'Bearer admin-secret', 162 + Authorization => $admin_auth, 162 163 } => json => { 163 164 subject => { did => $did }, 164 165 takedown => { applied => JSON::PP::false }, ··· 176 177 my $blob_cid = $blob->{ref}{'$link'}; 177 178 178 179 $t->post_ok('/xrpc/com.atproto.admin.updateSubjectStatus' => { 179 - Authorization => 'Bearer admin-secret', 180 + Authorization => $admin_auth, 180 181 } => json => { 181 182 subject => { did => $did, cid => $blob_cid }, 182 183 takedown => { applied => JSON::PP::true }, ··· 224 225 ->json_is('/error', 'BlobTakenDown'); 225 226 226 227 $t->get_ok('/xrpc/com.atproto.admin.getSubjectStatus' => { 227 - Authorization => 'Bearer admin-secret', 228 + Authorization => $admin_auth, 228 229 } => form => { 229 230 did => $did, 230 231 blob => $blob_cid,
+29
t/server-auth.t
··· 32 32 service_did_method => 'did:web', 33 33 service_handle_domain => 'localhost', 34 34 jwt_secret => 'test-secret', 35 + admin_password => 'admin-secret', 35 36 data_dir => $tmp, 36 37 db_path => File::Spec->catfile($tmp, 'perlsky.sqlite'), 37 38 }; ··· 55 56 my $access = $created->{accessJwt}; 56 57 my $refresh = $created->{refreshJwt}; 57 58 my $did = $created->{did}; 59 + my $admin_auth = 'Basic YWRtaW46YWRtaW4tc2VjcmV0'; 58 60 my ($account_id) = $did =~ /:users:([^:]+)\z/; 59 61 60 62 $t->get_ok('/xrpc/com.atproto.server.getSession' => { Authorization => "Bearer $access" }) 61 63 ->status_is(200) 62 64 ->json_is('/handle' => 'alice.localhost') 63 65 ->json_is('/email' => 'alice@example.com'); 66 + 67 + $t->get_ok('/xrpc/com.atproto.admin.getInviteCodes' => { 68 + Authorization => 'Bearer admin-secret', 69 + })->status_is(403) 70 + ->json_is('/error' => 'InvalidAdminToken'); 71 + 72 + $t->get_ok('/xrpc/com.atproto.admin.getInviteCodes' => { 73 + Authorization => $admin_auth, 74 + })->status_is(200) 75 + ->json_is('/codes' => []); 64 76 65 77 $t->get_ok("/xrpc/com.atproto.identity.resolveHandle?handle=alice.localhost") 66 78 ->status_is(200) ··· 232 244 $pk->verify_message_rfc7518(_b64url_decode($sig_b64), "$header_b64.$claims_b64", 'SHA256'), 233 245 'service auth signature verifies', 234 246 ); 247 + 248 + my $legacy_tmp = File::Spec->catdir($root, 'data', 'tmp-tests', 'server-auth-legacy'); 249 + remove_tree($legacy_tmp) if -d $legacy_tmp; 250 + my $legacy_t = Test::Mojo->new(ATProto::PDS->new( 251 + project_root => $root, 252 + settings => { 253 + %$config, 254 + data_dir => $legacy_tmp, 255 + db_path => File::Spec->catfile($legacy_tmp, 'perlsky.sqlite'), 256 + legacy_admin_bearer_auth => 1, 257 + }, 258 + )); 259 + 260 + $legacy_t->get_ok('/xrpc/com.atproto.admin.getInviteCodes' => { 261 + Authorization => 'Bearer admin-secret', 262 + })->status_is(200) 263 + ->json_is('/codes' => []); 235 264 236 265 done_testing; 237 266