Bläddra i källkod

Added email for users with expiring api tokens

Will Browning 4 år sedan
förälder
incheckning
2bfc353713

+ 5 - 1
.env.example

@@ -46,4 +46,8 @@ ANONADDY_ADDITIONAL_USERNAME_LIMIT=3
 ANONADDY_SIGNING_KEY_FINGERPRINT=
 # This is only needed if you will be adding any custom domains. If you do not need it then leave it blank. ANONADDY_DKIM_SIGNING_KEY=/etc/opendkim/keys/example.com/default.private
 ANONADDY_DKIM_SIGNING_KEY=
-ANONADDY_DKIM_SELECTOR=default
+ANONADDY_DKIM_SELECTOR=default
+
+# These details will be displayed after you run php artisan passport:install, you should update accordingly
+PASSPORT_PERSONAL_ACCESS_CLIENT_ID=client-id-value
+PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET=unhashed-client-secret-value

+ 36 - 2
SELF-HOSTING.md

@@ -430,6 +430,12 @@ listen.owner = johndoe
 listen.group = johndoe
 ```
 
+Then restart php7.4-fpm by running:
+
+```bash
+sudo service php7.4-fpm restart
+```
+
 ## Let's Encrypt
 
 Now we need to get an SSL certificate using Acme.sh.
@@ -503,10 +509,10 @@ Next follow this blog post on how to install OpenDMARC.
 
 [https://www.linuxbabe.com/mail-server/opendmarc-postfix-ubuntu](https://www.linuxbabe.com/mail-server/opendmarc-postfix-ubuntu)
 
-Next add a new TXT record to your domain for DMARC with a host of `@` and value:
+Next add a new TXT record to your domain for DMARC with a host of `_dmarc` and value:
 
 ```
-v=DMARC1; p=none; pct=100; rua=mailto:dmarc-reports@example.com
+"v=DMARC1; p=none; sp=none; adkim=r; aspf=r; pct=100;"
 ```
 
 For further reading about DMARC records and the different options available see - [https://www.linuxbabe.com/mail-server/create-dmarc-record](https://www.linuxbabe.com/mail-server/create-dmarc-record)
@@ -867,6 +873,34 @@ php artisan queue:restart
 php artisan passport:install
 ```
 
+Running `passport:install` will output details about a new personal access client, e.g.
+
+```bash
+Encryption keys generated successfully.
+Personal access client created successfully.
+Client ID: 1
+Client secret: MlVp37PNqtN9efBTw2wuenjMnMIlDuKBWK3GZQoJ
+Password grant client created successfully.
+Client ID: 2
+Client secret: ZTvhZCRZMdKUvmwqSmNAfWzAoaRatVWgbCVN2cR2
+```
+
+You need to update your `.env` file and add the details for the personal access client:
+
+```
+PASSPORT_PERSONAL_ACCESS_CLIENT_ID=client-id-value
+PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET=unhashed-client-secret-value
+```
+
+So I would enter:
+
+```
+PASSPORT_PERSONAL_ACCESS_CLIENT_ID=1
+PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET=MlVp37PNqtN9efBTw2wuenjMnMIlDuKBWK3GZQoJ
+```
+
+More information can be found in the Laravel documentation for Passport - [https://laravel.com/docs/8.x/passport](https://laravel.com/docs/8.x/passport)
+
 ## Installing Supervisor
 
 We will be using supervisor for keeping the Laravel queue worker alive.

+ 63 - 0
app/Console/Commands/EmailUsersWithTokenExpiringSoon.php

@@ -0,0 +1,63 @@
+<?php
+
+namespace App\Console\Commands;
+
+use App\Mail\TokenExpiringSoon;
+use App\Models\User;
+use Exception;
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\Mail;
+
+class EmailUsersWithTokenExpiringSoon extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'anonaddy:email-users-with-token-expiring-soon';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Send an email to users who have an API token that is expiring soon';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        User::whereHas('tokens', function ($query) {
+            $query->whereDate('expires_at', now()->addWeek())
+                ->where('revoked', false);
+        })
+        ->get()
+        ->each(function (User $user) {
+            $this->sendTokenExpiringSoonMail($user);
+        });
+    }
+
+    protected function sendTokenExpiringSoonMail(User $user)
+    {
+        try {
+            Mail::to($user->email)->send(new TokenExpiringSoon($user));
+        } catch (Exception $exception) {
+            $this->error("exception when sending mail to user: {$user->username}", $exception);
+            report($exception);
+        }
+    }
+}

+ 1 - 0
app/Console/Kernel.php

@@ -25,6 +25,7 @@ class Kernel extends ConsoleKernel
     protected function schedule(Schedule $schedule)
     {
         $schedule->command('anonaddy:reset-bandwidth')->monthlyOn(1, '00:00');
+        $schedule->command('anonaddy:email-users-with-token-expiring-soon')->dailyAt('12:00');
         $schedule->command('auth:clear-resets')->daily();
     }
 

+ 1 - 1
app/Http/Controllers/ShowAliasController.php

@@ -21,7 +21,7 @@ class ShowAliasController extends Controller
             'aliases' => user()
                 ->aliases()
                 ->with([
-                    'recipients:recipient_id,email',
+                    'recipients:id,email',
                     'aliasable.defaultRecipient:id,email'
                 ])
                 ->latest()

+ 1 - 1
app/Http/Controllers/ShowRecipientController.php

@@ -7,7 +7,7 @@ class ShowRecipientController extends Controller
     public function index()
     {
         $recipients = user()->recipients()->with([
-            'aliases:alias_id,aliasable_id,email',
+            'aliases:id,aliasable_id,email',
             'domainsUsingAsDefault.aliases:id,aliasable_id,email',
             'AdditionalUsernamesUsingAsDefault.aliases:id,aliasable_id,email'
         ])->latest()->get();

+ 40 - 0
app/Mail/TokenExpiringSoon.php

@@ -0,0 +1,40 @@
+<?php
+
+namespace App\Mail;
+
+use App\Models\User;
+use Illuminate\Bus\Queueable;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Mail\Mailable;
+use Illuminate\Queue\SerializesModels;
+
+class TokenExpiringSoon extends Mailable implements ShouldQueue
+{
+    use Queueable, SerializesModels;
+
+    protected $user;
+
+    /**
+     * Create a new message instance.
+     *
+     * @return void
+     */
+    public function __construct(User $user)
+    {
+        $this->user = $user;
+    }
+
+    /**
+     * Build the message.
+     *
+     * @return $this
+     */
+    public function build()
+    {
+        return $this
+            ->subject("Your AnonAddy API token expires soon")
+            ->markdown('mail.token_expiring_soon', [
+                'user' => $this->user
+            ]);
+    }
+}

+ 21 - 25
app/Models/User.php

@@ -323,31 +323,27 @@ class User extends Authenticatable implements MustVerifyEmail
     {
         $gnupg = new \gnupg();
 
-        $key = $gnupg->keyinfo($fingerprint);
-
-        // Check that the user has a verified recipient matching the keys email.
-        collect($key[0]['uids'])
-            ->filter(function ($uid) {
-                return ! $uid['invalid'];
-            })
-            ->pluck('email')
-            ->each(function ($email) use ($gnupg, $fingerprint) {
-                if ($this->isVerifiedRecipient($email)) {
-                    $gnupg->deletekey($fingerprint);
-                }
-            });
-
-        // Remove the key from all user recipients using that same fingerprint.
-        if (! $gnupg->keyinfo($fingerprint)) {
-            $this
-                ->recipients()
-                ->get()
-                ->where('fingerprint', $fingerprint)
-                ->each(function ($recipient) {
-                    $recipient->update([
-                        'should_encrypt' => false,
-                        'fingerprint' => null
-                    ]);
+        $recipientsUsingFingerprint = $this
+            ->recipients()
+            ->get()
+            ->where('fingerprint', $fingerprint);
+
+        // Check that the user has a verified recipient matching the key's email and if any other recipients are using that key.
+        if (isset($key[0])) {
+            collect($key[0]['uids'])
+                ->filter(function ($uid) {
+                    return ! $uid['invalid'];
+                })
+                ->pluck('email')
+                ->each(function ($email) use ($gnupg, $fingerprint, $recipientsUsingFingerprint) {
+                    if ($this->isVerifiedRecipient($email) && $recipientsUsingFingerprint->count() === 1) {
+                        $gnupg->deletekey($fingerprint);
+
+                        $recipientsUsingFingerprint->first()->update([
+                            'should_encrypt' => false,
+                            'fingerprint' => null
+                        ]);
+                    }
                 });
         }
     }

+ 2 - 0
app/Providers/AuthServiceProvider.php

@@ -30,5 +30,7 @@ class AuthServiceProvider extends ServiceProvider
         }, ['middleware' => ['web', 'auth', '2fa']]);
 
         Passport::cookie('anonaddy_token');
+
+        Passport::personalAccessTokensExpireIn(now()->addYears(5));
     }
 }

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 162 - 203
composer.lock


+ 2 - 2
config/version.yml

@@ -5,9 +5,9 @@ current:
   major: 0
   minor: 6
   patch: 0
-  prerelease: ''
+  prerelease: 1-geaed93a
   buildmetadata: ''
-  commit: f00b28
+  commit: eaed93
   timestamp:
     year: 2020
     month: 10

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 625 - 427
package-lock.json


+ 4 - 5
package.json

@@ -11,16 +11,15 @@
         "format": "prettier --write 'resources/**/*.{css,js,vue}'"
     },
     "dependencies": {
-        "axios": "^0.18.1",
-        "cross-env": "^5.2.1",
+        "axios": "^0.19",
+        "cross-env": "^7.0",
         "dayjs": "^1.9.3",
-        "laravel-mix": "^4.1.4",
-        "laravel-mix-purgecss": "^4.2.0",
+        "laravel-mix": "^5.0.7",
         "lodash": "^4.17.20",
         "portal-vue": "^2.1.7",
         "postcss-import": "^11.1.0",
         "postcss-nesting": "^5.0.0",
-        "resolve-url-loader": "^2.3.2",
+        "resolve-url-loader": "^3.1.2",
         "tailwindcss": "^1.9.5",
         "tippy.js": "^4.3.5",
         "v-clipboard": "^2.2.3",

+ 4 - 11
resources/js/pages/Recipients.vue

@@ -537,12 +537,8 @@ export default {
       axios
         .delete(`/api/v1/recipients/${recipient.id}`)
         .then(response => {
-          let recipients = _.filter(this.rows, ['fingerprint', recipient.fingerprint])
-
-          _.forEach(recipients, function(recipient) {
-            recipient.should_encrypt = false
-            recipient.fingerprint = null
-          })
+          recipient.should_encrypt = false
+          recipient.fingerprint = null
 
           this.rows = _.reject(this.rows, row => row.id === recipient.id)
           this.deleteRecipientModalOpen = false
@@ -568,12 +564,9 @@ export default {
       axios
         .delete(`/api/v1/recipient-keys/${recipient.id}`)
         .then(response => {
-          let recipients = _.filter(this.rows, ['fingerprint', recipient.fingerprint])
+          recipient.should_encrypt = false
+          recipient.fingerprint = null
 
-          _.forEach(recipients, function(recipient) {
-            recipient.should_encrypt = false
-            recipient.fingerprint = null
-          })
           this.deleteRecipientKeyModalOpen = false
           this.deleteRecipientKeyLoading = false
         })

+ 13 - 0
resources/views/mail/token_expiring_soon.blade.php

@@ -0,0 +1,13 @@
+@component('mail::message')
+
+# Your API token expires soon
+
+One of the API tokens on your AnonAddy account will expire in **one weeks time**.</p>
+
+If you are not using this API token for the browser extension or to access the API then you do not need to take any action.
+
+If you **are using the token** for the browser extension please log into your account and generate a new API token and add this to the browser extension before your current one expires.
+
+Once an API token has expired it can no longer be used to access the API.
+
+@endcomponent

+ 14 - 0
tailwind.config.js

@@ -3,6 +3,20 @@ module.exports = {
     removeDeprecatedGapUtilities: true,
     purgeLayersByDefault: true,
   },
+  purge: {
+    content: [
+      'app/**/*.php',
+      'resources/**/*.html',
+      'resources/**/*.js',
+      'resources/**/*.php',
+      'resources/**/*.vue',
+    ],
+
+    // These options are passed through directly to PurgeCSS
+    options: {
+      whitelistPatterns: [/-active$/, /-enter$/, /-leave-to$/, /show$/],
+    },
+  },
   theme: {
     colors: {
       white: '#FFF',

+ 3 - 62
tests/Feature/ReceiveEmailTest.php

@@ -531,7 +531,7 @@ class ReceiveEmailTest extends TestCase
             'emails_blocked' => 0
         ]);
 
-        Mail::assertNotSent(ForwardEmail::class);
+        Mail::assertNotQueued(ForwardEmail::class);
     }
 
     /** @test */
@@ -583,66 +583,7 @@ class ReceiveEmailTest extends TestCase
             'active' => false
         ]);
 
-        Mail::assertNotSent(ForwardEmail::class);
-    }
-
-    /** @test */
-    public function it_does_not_count_unsubscribe_recipient_when_calculating_size()
-    {
-        Mail::fake();
-
-        Mail::assertNothingSent();
-
-        Alias::factory()->create([
-            'id' => '8f36380f-df4e-4875-bb12-9c4448573712',
-            'user_id' => $this->user->id,
-            'email' => 'ebay@johndoe.'.config('anonaddy.domain'),
-            'local_part' => 'ebay',
-            'domain' => 'johndoe.'.config('anonaddy.domain')
-        ]);
-
-        Recipient::factory()->create([
-            'user_id' => $this->user->id,
-            'email' => 'will@anonaddy.com'
-        ]);
-
-        $this->assertDatabaseHas('aliases', [
-            'id' => '8f36380f-df4e-4875-bb12-9c4448573712',
-            'user_id' => $this->user->id,
-            'email' => 'ebay@johndoe.'.config('anonaddy.domain'),
-            'active' => true
-        ]);
-
-        $this->artisan(
-            'anonaddy:receive-email',
-            [
-                'file' => base_path('tests/emails/email_unsubscribe_plus_other_recipient.eml'),
-                '--sender' => 'will@anonaddy.com',
-                '--recipient' => ['8f36380f-df4e-4875-bb12-9c4448573712@unsubscribe.anonaddy.com', 'another@johndoe.anonaddy.com'],
-                '--local_part' => ['8f36380f-df4e-4875-bb12-9c4448573712', 'another'],
-                '--extension' => ['', ''],
-                '--domain' => ['unsubscribe.anonaddy.com', 'johndoe.anonaddy.com'],
-                '--size' => '1000'
-            ]
-        )->assertExitCode(0);
-
-        $this->assertDatabaseHas('aliases', [
-            'id' => '8f36380f-df4e-4875-bb12-9c4448573712',
-            'user_id' => $this->user->id,
-            'email' => 'ebay@johndoe.'.config('anonaddy.domain'),
-            'local_part' => 'ebay',
-            'domain' => 'johndoe.'.config('anonaddy.domain'),
-            'active' => false
-        ]);
-        $this->assertDatabaseHas('aliases', [
-            'user_id' => $this->user->id,
-            'email' => 'another@johndoe.'.config('anonaddy.domain'),
-            'local_part' => 'another',
-            'domain' => 'johndoe.'.config('anonaddy.domain'),
-            'active' => true
-        ]);
-
-        Mail::assertNotSent(ForwardEmail::class);
+        Mail::assertNotQueued(ForwardEmail::class);
     }
 
     /** @test */
@@ -689,7 +630,7 @@ class ReceiveEmailTest extends TestCase
             'active' => true
         ]);
 
-        Mail::assertNotSent(ForwardEmail::class);
+        Mail::assertNotQueued(ForwardEmail::class);
     }
 
     /** @test */

+ 62 - 0
tests/Unit/EmailUsersWithTokenExpiringSoonTest.php

@@ -0,0 +1,62 @@
+<?php
+
+namespace Tests\Unit;
+
+use App\Mail\TokenExpiringSoon;
+use Carbon\Carbon;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use Illuminate\Support\Facades\Mail;
+use Tests\TestCase;
+
+class EmailUsersWithTokenExpiringSoonTest extends TestCase
+{
+    use RefreshDatabase;
+
+    protected $user;
+
+    protected function setUp(): void
+    {
+        parent::setUp();
+        parent::setUpPassport();
+        $this->user->tokens()->first()->update(['expires_at' => Carbon::create(2019, 1, 31)]);
+
+        Mail::fake();
+    }
+
+    /** @test */
+    public function it_can_send_a_mail_concerning_a_token_expiring_soon()
+    {
+        $this->setNow(2019, 1, 28);
+        $this->artisan('anonaddy:email-users-with-token-expiring-soon');
+        Mail::assertNotQueued(TokenExpiringSoon::class);
+
+        $this->setNow(2019, 1, 29);
+        $this->artisan('anonaddy:email-users-with-token-expiring-soon');
+        Mail::assertNotQueued(TokenExpiringSoon::class);
+
+        $this->setNow(2019, 1, 24);
+        $this->artisan('anonaddy:email-users-with-token-expiring-soon');
+        Mail::assertQueued(TokenExpiringSoon::class, 1);
+        Mail::assertQueued(TokenExpiringSoon::class, function (TokenExpiringSoon $mail) {
+            return $mail->hasTo($this->user->email);
+        });
+    }
+
+    /** @test */
+    public function it_does_not_send_a_mail_for_revoked_tokens()
+    {
+        $this->user->tokens()->first()->revoke();
+        $this->setNow(2019, 1, 24);
+        $this->artisan('anonaddy:email-users-with-token-expiring-soon');
+        Mail::assertNotQueued(TokenExpiringSoon::class);
+    }
+
+    protected function setNow(int $year, int $month, int $day)
+    {
+        $newNow = Carbon::create($year, $month, $day)->startOfDay();
+
+        Carbon::setTestNow($newNow);
+
+        return $this;
+    }
+}

+ 0 - 15
tests/emails/email_unsubscribe_plus_other_recipient.eml

@@ -1,15 +0,0 @@
-Date: Wed, 20 Feb 2019 15:00:00 +0100 (CET)
-From: Will <will@anonaddy.com>
-To: <8f36380f-df4e-4875-bb12-9c4448573712@unsubscribe.anonaddy.com>, <another@johndoe.anonaddy.com>
-Subject: Unsubscribe
-Content-Type: multipart/mixed; boundary="----=_Part_10031_1199410393.1550677940425"
-
-------=_Part_10031_1199410393.1550677940425
-Content-Type: text/html; charset=UTF-8
-Content-Transfer-Encoding: quoted-printable
-
-------=_Part_10031_1199410393.1550677940425
-Content-Type: text/plain; charset=UTF-8
-Content-Transfer-Encoding: quoted-printable
-
-------=_Part_10031_1199410393.1550677940425--

+ 4 - 5
webpack.mix.js

@@ -1,16 +1,15 @@
 let mix = require('laravel-mix')
-require('laravel-mix-purgecss')
 
-mix.js('resources/js/app.js', 'public/js')
+mix
+  .js('resources/js/app.js', 'public/js')
   .postCss('resources/css/app.css', 'public/css')
   .options({
     postCss: [
       require('postcss-import')(),
-      require('tailwindcss')( './tailwind.config.js' ),
+      require('tailwindcss')('./tailwind.config.js'),
       require('postcss-nesting')(),
-    ]
+    ],
   })
-  .purgeCss()
 
 if (mix.inProduction()) {
   mix.version()

Vissa filer visades inte eftersom för många filer har ändrats