Projet

Général

Profil

0001-general-remove-fargo.oauth2-67570.patch

Frédéric Péters, 21 juillet 2022 08:52

Télécharger (65 ko)

Voir les différences:

Subject: [PATCH] general: remove fargo.oauth2 (#67570)

 fargo/fargo/managers.py                       |   2 +-
 fargo/oauth2/__init__.py                      |   0
 fargo/oauth2/admin.py                         |  73 -----
 fargo/oauth2/authentication.py                | 104 -------
 fargo/oauth2/forms.py                         |  34 ---
 fargo/oauth2/management/__init__.py           |   0
 fargo/oauth2/management/commands/__init__.py  |   0
 .../commands/oauth2-create-client.py          |  40 ---
 .../commands/oauth2-put-document.py           |  47 ---
 fargo/oauth2/migrations/0001_initial.py       |  60 ----
 .../0001_squashed_0005_auto_20180331_1532.py  | 107 -------
 .../migrations/0002_auto_20180321_2343.py     |  42 ---
 .../migrations/0003_auto_20180322_1016.py     |  31 --
 .../migrations/0004_auto_20180326_1330.py     |  23 --
 .../migrations/0005_auto_20180331_1532.py     |  37 ---
 fargo/oauth2/migrations/__init__.py           |   0
 fargo/oauth2/models.py                        | 120 --------
 fargo/oauth2/urls.py                          |  35 ---
 fargo/oauth2/utils.py                         |  60 ----
 fargo/oauth2/views.py                         | 279 ------------------
 fargo/settings.py                             |   3 -
 fargo/templates/fargo/oauth2/authorize.html   |  22 --
 fargo/templates/fargo/oauth2/confirm.html     |  33 ---
 fargo/urls.py                                 |   1 -
 tests/conftest.py                             |   4 +-
 tests/test_commands.py                        |  20 +-
 tests/test_file.txt                           |   6 +
 tests/test_oauth2.py                          | 265 -----------------
 tests/test_oauth2.txt                         |  36 ---
 29 files changed, 11 insertions(+), 1473 deletions(-)
 delete mode 100644 fargo/oauth2/__init__.py
 delete mode 100644 fargo/oauth2/admin.py
 delete mode 100644 fargo/oauth2/authentication.py
 delete mode 100644 fargo/oauth2/forms.py
 delete mode 100644 fargo/oauth2/management/__init__.py
 delete mode 100644 fargo/oauth2/management/commands/__init__.py
 delete mode 100644 fargo/oauth2/management/commands/oauth2-create-client.py
 delete mode 100644 fargo/oauth2/management/commands/oauth2-put-document.py
 delete mode 100644 fargo/oauth2/migrations/0001_initial.py
 delete mode 100644 fargo/oauth2/migrations/0001_squashed_0005_auto_20180331_1532.py
 delete mode 100644 fargo/oauth2/migrations/0002_auto_20180321_2343.py
 delete mode 100644 fargo/oauth2/migrations/0003_auto_20180322_1016.py
 delete mode 100644 fargo/oauth2/migrations/0004_auto_20180326_1330.py
 delete mode 100644 fargo/oauth2/migrations/0005_auto_20180331_1532.py
 delete mode 100644 fargo/oauth2/migrations/__init__.py
 delete mode 100644 fargo/oauth2/models.py
 delete mode 100644 fargo/oauth2/urls.py
 delete mode 100644 fargo/oauth2/utils.py
 delete mode 100644 fargo/oauth2/views.py
 delete mode 100644 fargo/templates/fargo/oauth2/authorize.html
 delete mode 100644 fargo/templates/fargo/oauth2/confirm.html
 create mode 100644 tests/test_file.txt
 delete mode 100644 tests/test_oauth2.py
 delete mode 100644 tests/test_oauth2.txt
fargo/fargo/managers.py
28 28
        n = n or now()
29 29
        # use a window of 60 seconds to be sure this document will never be used
30 30
        qs = self.filter(creation_date__lt=n - datetime.timedelta(seconds=60))
31
        qs = qs.filter(user_documents__isnull=True, oauth2_tempfiles__isnull=True)
31
        qs = qs.filter(user_documents__isnull=True)
32 32
        for document in qs:
33 33
            document.content.delete(False)
34 34
        qs.delete()
fargo/oauth2/admin.py
1
# fargo - document box
2
# Copyright (C) 2016-2019  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from django.contrib import admin
18
from django.utils.translation import ugettext_lazy as _
19

  
20
from .models import OAuth2Authorize, OAuth2Client, OAuth2TempFile
21

  
22

  
23
class OAuth2ClientAdmin(admin.ModelAdmin):
24
    fields = ('client_name', 'client_id', 'client_secret', 'redirect_uris')
25
    list_display = ['client_name', 'client_id', 'client_secret', 'redirect_uris']
26

  
27

  
28
class OAuth2AuthorizeAdmin(admin.ModelAdmin):
29
    list_display = [
30
        'id',
31
        'client_name',
32
        'user_document',
33
        'thumbnail',
34
        'access_token',
35
        'code',
36
        'creation_date',
37
    ]
38
    raw_id_fields = ['user_document']
39
    search_fields = [
40
        'client__client_name',
41
        'user_document__user__email',
42
        'user_document__user__first_name',
43
        'user_document__user__last_name',
44
        'user_document__filename',
45
        'user_document__user__contenat_has',
46
    ]
47

  
48
    def thumbnail(self, instance):
49
        return instance.user_document.document.thumbnail_img_tag
50

  
51
    thumbnail.short_description = _('thumbnail')
52

  
53
    def client_name(self, instance):
54
        return instance.client.client_name
55

  
56

  
57
class OAuth2TempFileAdmin(admin.ModelAdmin):
58
    list_display = ['uuid', 'client_name', 'filename', 'thumbnail', 'creation_date']
59
    raw_id_fields = ['document']
60
    search_fields = ['filename', 'uuid', 'client__client_name']
61

  
62
    def thumbnail(self, instance):
63
        return instance.document.thumbnail_img_tag
64

  
65
    thumbnail.short_description = _('thumbnail')
66

  
67
    def client_name(self, instance):
68
        return instance.client.client_name
69

  
70

  
71
admin.site.register(OAuth2Client, OAuth2ClientAdmin)
72
admin.site.register(OAuth2Authorize, OAuth2AuthorizeAdmin)
73
admin.site.register(OAuth2TempFile, OAuth2TempFileAdmin)
fargo/oauth2/authentication.py
1
# fargo - document box
2
# Copyright (C) 2016-2019  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import logging
18

  
19
import requests
20
from django.conf import settings
21
from django.utils.six.moves.urllib import parse as urlparse
22
from django.utils.translation import ugettext_lazy as _
23
from rest_framework.authentication import BasicAuthentication
24
from rest_framework.exceptions import AuthenticationFailed
25

  
26
from .models import OAuth2Client
27

  
28

  
29
class OAuth2User:
30
    """Fake user class to return in case OAuth2 Client authentication"""
31

  
32
    def __init__(self, oauth2_client):
33
        self.oauth2_client = oauth2_client
34
        self.authenticated = False
35

  
36
    def has_perm(self, *args, **kwargs):
37
        return True
38

  
39
    def has_perm_any(self, *args, **kwargs):
40
        return True
41

  
42
    def has_ou_perm(self, *args, **kwargs):
43
        return True
44

  
45
    def filter_by_perm(self, perms, queryset):
46
        return queryset
47

  
48
    def is_authenticated(self):
49
        return self.authenticated
50

  
51
    def is_staff(self):
52
        return False
53

  
54

  
55
class FargoOAUTH2Authentication(BasicAuthentication):
56
    def authenticate_through_idp(self, client_id, client_secret):
57
        """Check client_id and client_secret with configured IdP, and verify it is an OIDC
58
        client.
59
        """
60
        logger = logging.getLogger(__name__)
61

  
62
        authentic_idp = getattr(settings, 'FARGO_IDP_URL', None)
63
        if not authentic_idp:
64
            logger.warning('idp check-password not configured')
65
            return False, ''
66

  
67
        url = urlparse.urljoin(authentic_idp, 'api/check-password/')
68
        try:
69
            response = requests.post(
70
                url,
71
                json={'username': client_id, 'password': client_secret},
72
                auth=(client_id, client_secret),
73
                verify=False,
74
            )
75
            response.raise_for_status()
76
        except requests.RequestException as e:
77
            logger.warning('idp check-password API failed: %s', e)
78
            return False, 'idp is down'
79
        try:
80
            response = response.json()
81
        except ValueError as e:
82
            logger.warning('idp check-password API failed: %s, %r', e, response.content)
83
            return False, 'idp is down'
84

  
85
        if response.get('result') == 0:
86
            logger.warning('idp check-password API failed')
87
            return False, response.get('errors', [''])[0]
88

  
89
        return True, None
90

  
91
    def authenticate_credentials(self, client_id, client_secret, request=None):
92
        try:
93
            client = OAuth2Client.objects.get(client_id=client_id, client_secret=client_secret)
94
        except OAuth2Client.DoesNotExist:
95
            success, error = self.authenticate_through_idp(client_id, client_secret)
96
            if not success:
97
                raise AuthenticationFailed(error or _('Invalid client_id/client_secret.'))
98
            client = OAuth2Client.objects.get(client_id=client_id)
99
            client.client_secret = client_secret
100
            client.save()
101

  
102
        user = OAuth2User(client)
103
        user.authenticated = True
104
        return user, True
fargo/oauth2/forms.py
1
# fargo - document box
2
# Copyright (C) 2016-2019  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from django import forms
18
from django.utils.translation import ugettext_lazy as _
19

  
20
from fargo.fargo.models import UserDocument
21

  
22

  
23
class UserDocModelChoiceField(forms.ModelChoiceField):
24
    def label_from_instance(self, obj):
25
        return obj.filename
26

  
27

  
28
class OAuth2AuthorizeForm(forms.Form):
29
    document = UserDocModelChoiceField(queryset=None)
30

  
31
    def __init__(self, user, *args, **kwargs):
32
        super().__init__(*args, **kwargs)
33
        self.fields['document'].queryset = UserDocument.objects.filter(user=user)
34
        self.fields['document'].label = _('Document')
fargo/oauth2/management/commands/oauth2-create-client.py
1
# fargo - document box
2
# Copyright (C) 2016-2019  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from django.core.management.base import BaseCommand
18

  
19
from fargo.oauth2.models import OAuth2Client
20

  
21

  
22
class Command(BaseCommand):
23
    help = 'Create an OAuth2 client'
24

  
25
    def add_arguments(self, parser):
26
        parser.add_argument('client_name')
27
        parser.add_argument('redirect_uris')
28
        parser.add_argument('--client-id', required=False, default=None)
29
        parser.add_argument('--client-secret', required=False, default=None)
30

  
31
    def handle(self, client_name, redirect_uris, client_id, client_secret, **options):
32
        kwargs = {
33
            'client_name': client_name,
34
            'redirect_uris': redirect_uris,
35
        }
36
        if client_id:
37
            kwargs['client_id'] = client_id
38
        if client_secret:
39
            kwargs['client_secret'] = client_secret
40
        OAuth2Client.objects.create(**kwargs)
fargo/oauth2/management/commands/oauth2-put-document.py
1
# fargo - document box
2
# Copyright (C) 2016-2019  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import os
18

  
19
from django.core.files.base import ContentFile
20
from django.core.management.base import BaseCommand
21
from django.urls import reverse
22

  
23
from fargo.fargo.models import Document
24
from fargo.oauth2.models import OAuth2Client, OAuth2TempFile
25
from fargo.utils import make_url
26

  
27

  
28
class Command(BaseCommand):
29
    help = 'Push documents inside fargo, returns URLs'
30

  
31
    def add_arguments(self, parser):
32
        parser.add_argument('--client-id', type=int)
33
        parser.add_argument('redirect_uri')
34
        parser.add_argument('paths', nargs='+')
35

  
36
    def handle(self, redirect_uri, paths, client_id, **options):
37
        client = OAuth2Client.objects.get(id=client_id)
38
        for path in paths:
39
            with open(path, 'rb') as file_object:
40
                filename = os.path.basename(path)
41
                f = ContentFile(file_object.read(), name=filename)
42
                document = Document.objects.get_by_file(f)
43
                oauth2_document = OAuth2TempFile.objects.create(
44
                    client=client, document=document, filename=filename
45
                )
46
                uri = reverse('oauth2-put-document-authorize', args=[oauth2_document.pk])
47
                self.stdout.write('https://localhost:8000' + make_url(uri, redirect_uri=redirect_uri))
fargo/oauth2/migrations/0001_initial.py
1
from django.db import migrations, models
2

  
3
import fargo.oauth2.models
4

  
5

  
6
class Migration(migrations.Migration):
7

  
8
    dependencies = [
9
        ('fargo', '0013_document_mime_type'),
10
    ]
11

  
12
    operations = [
13
        migrations.CreateModel(
14
            name='OAuth2Authorize',
15
            fields=[
16
                (
17
                    'id',
18
                    models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
19
                ),
20
                ('access_token', models.CharField(default=fargo.oauth2.models.generate_uuid, max_length=255)),
21
                ('code', models.CharField(default=fargo.oauth2.models.generate_uuid, max_length=255)),
22
                ('creation_date', models.DateTimeField(auto_now=True)),
23
                ('user_document', models.ForeignKey(to='fargo.UserDocument', on_delete=models.CASCADE)),
24
            ],
25
        ),
26
        migrations.CreateModel(
27
            name='OAuth2Client',
28
            fields=[
29
                (
30
                    'id',
31
                    models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
32
                ),
33
                (
34
                    'client_secret',
35
                    models.CharField(default=fargo.oauth2.models.generate_uuid, max_length=255),
36
                ),
37
                ('client_id', models.CharField(default=fargo.oauth2.models.generate_uuid, max_length=255)),
38
                ('client_name', models.CharField(max_length=255)),
39
                (
40
                    'redirect_uris',
41
                    models.TextField(
42
                        verbose_name='redirect URIs', validators=[fargo.oauth2.models.validate_https_url]
43
                    ),
44
                ),
45
            ],
46
        ),
47
        migrations.CreateModel(
48
            name='OAuth2TempFile',
49
            fields=[
50
                ('hash_key', models.CharField(max_length=128, serialize=False, primary_key=True)),
51
                ('filename', models.CharField(max_length=512)),
52
                (
53
                    'document',
54
                    models.ForeignKey(
55
                        to='fargo.Document', related_name='oauth2_tempfiles', on_delete=models.CASCADE
56
                    ),
57
                ),
58
            ],
59
        ),
60
    ]
fargo/oauth2/migrations/0001_squashed_0005_auto_20180331_1532.py
1
# Generated by Django 1.11.11 on 2018-03-31 13:36
2

  
3
import django.db.models.deletion
4
from django.db import migrations, models
5

  
6
import fargo.oauth2.models
7

  
8

  
9
class Migration(migrations.Migration):
10

  
11
    replaces = [
12
        ('oauth2', '0001_initial'),
13
        ('oauth2', '0002_auto_20180321_2343'),
14
        ('oauth2', '0003_auto_20180322_1016'),
15
        ('oauth2', '0004_auto_20180326_1330'),
16
        ('oauth2', '0005_auto_20180331_1532'),
17
    ]
18

  
19
    initial = True
20

  
21
    dependencies = [
22
        ('fargo', '0013_document_mime_type'),
23
    ]
24

  
25
    operations = [
26
        migrations.CreateModel(
27
            name='OAuth2Authorize',
28
            fields=[
29
                (
30
                    'id',
31
                    models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
32
                ),
33
                ('access_token', models.CharField(default=fargo.oauth2.models.generate_uuid, max_length=255)),
34
                ('code', models.CharField(default=fargo.oauth2.models.generate_uuid, max_length=255)),
35
                ('creation_date', models.DateTimeField(auto_now_add=True)),
36
            ],
37
            options={
38
                'ordering': ('creation_date',),
39
                'verbose_name': 'OAUTH2 authorization',
40
                'verbose_name_plural': 'OAUTH2 authorizations',
41
            },
42
        ),
43
        migrations.CreateModel(
44
            name='OAuth2Client',
45
            fields=[
46
                (
47
                    'id',
48
                    models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
49
                ),
50
                ('client_name', models.CharField(max_length=255)),
51
                (
52
                    'redirect_uris',
53
                    models.TextField(
54
                        verbose_name='redirect URIs', validators=[fargo.oauth2.models.validate_https_url]
55
                    ),
56
                ),
57
                ('client_id', models.CharField(default=fargo.oauth2.models.generate_uuid, max_length=255)),
58
                (
59
                    'client_secret',
60
                    models.CharField(default=fargo.oauth2.models.generate_uuid, max_length=255),
61
                ),
62
            ],
63
            options={
64
                'ordering': ('client_name',),
65
                'verbose_name': 'OAUTH2 client',
66
                'verbose_name_plural': 'OAUTH2 clients',
67
            },
68
        ),
69
        migrations.CreateModel(
70
            name='OAuth2TempFile',
71
            fields=[
72
                (
73
                    'uuid',
74
                    models.CharField(
75
                        default=fargo.oauth2.models.generate_uuid,
76
                        max_length=32,
77
                        serialize=False,
78
                        primary_key=True,
79
                    ),
80
                ),
81
                ('filename', models.CharField(max_length=512)),
82
                ('creation_date', models.DateTimeField(auto_now_add=True)),
83
                ('client', models.ForeignKey(to='oauth2.OAuth2Client', on_delete=models.CASCADE)),
84
                (
85
                    'document',
86
                    models.ForeignKey(
87
                        related_name='oauth2_tempfiles', to='fargo.Document', on_delete=models.CASCADE
88
                    ),
89
                ),
90
            ],
91
            options={
92
                'ordering': ('creation_date',),
93
                'verbose_name': 'OAUTH2 temporary file',
94
                'verbose_name_plural': 'OAUTH2 temporary files',
95
            },
96
        ),
97
        migrations.AddField(
98
            model_name='oauth2authorize',
99
            name='client',
100
            field=models.ForeignKey(to='oauth2.OAuth2Client', on_delete=models.CASCADE),
101
        ),
102
        migrations.AddField(
103
            model_name='oauth2authorize',
104
            name='user_document',
105
            field=models.ForeignKey(to='fargo.UserDocument', on_delete=models.CASCADE),
106
        ),
107
    ]
fargo/oauth2/migrations/0002_auto_20180321_2343.py
1
# Generated by Django 1.11.11 on 2018-03-21 23:43
2

  
3
import django.db.models.deletion
4
from django.db import migrations, models
5

  
6

  
7
def delete_all_client_linked_models(apps, schema_editor):
8
    OAuth2Authorize = apps.get_model('oauth2', 'OAuth2Authorize')
9
    OAuth2TempFile = apps.get_model('oauth2', 'OAuth2TempFile')
10
    OAuth2Authorize.objects.all().delete()
11
    OAuth2TempFile.objects.all().delete()
12

  
13

  
14
def noop(apps, schema_editor):
15
    pass
16

  
17

  
18
class Migration(migrations.Migration):
19

  
20
    dependencies = [
21
        ('oauth2', '0001_initial'),
22
    ]
23

  
24
    operations = [
25
        migrations.RunPython(delete_all_client_linked_models, noop),
26
        migrations.AddField(
27
            model_name='oauth2authorize',
28
            name='client',
29
            field=models.ForeignKey(
30
                default=1, on_delete=django.db.models.deletion.CASCADE, to='oauth2.OAuth2Client'
31
            ),
32
            preserve_default=False,
33
        ),
34
        migrations.AddField(
35
            model_name='oauth2tempfile',
36
            name='client',
37
            field=models.ForeignKey(
38
                default=1, on_delete=django.db.models.deletion.CASCADE, to='oauth2.OAuth2Client'
39
            ),
40
            preserve_default=False,
41
        ),
42
    ]
fargo/oauth2/migrations/0003_auto_20180322_1016.py
1
# Generated by Django 1.11.11 on 2018-03-22 10:16
2

  
3
from django.db import migrations, models
4

  
5
import fargo.oauth2.models
6

  
7

  
8
class Migration(migrations.Migration):
9

  
10
    dependencies = [
11
        ('oauth2', '0002_auto_20180321_2343'),
12
    ]
13

  
14
    operations = [
15
        migrations.RemoveField(
16
            model_name='oauth2tempfile',
17
            name='hash_key',
18
        ),
19
        migrations.AddField(
20
            model_name='oauth2tempfile',
21
            name='creation_date',
22
            field=models.DateTimeField(auto_now=True),
23
        ),
24
        migrations.AddField(
25
            model_name='oauth2tempfile',
26
            name='uuid',
27
            field=models.CharField(
28
                default=fargo.oauth2.models.generate_uuid, max_length=32, primary_key=True, serialize=False
29
            ),
30
        ),
31
    ]
fargo/oauth2/migrations/0004_auto_20180326_1330.py
1
# Generated by Django 1.11.11 on 2018-03-26 13:30
2

  
3
from django.db import migrations, models
4

  
5

  
6
class Migration(migrations.Migration):
7

  
8
    dependencies = [
9
        ('oauth2', '0003_auto_20180322_1016'),
10
    ]
11

  
12
    operations = [
13
        migrations.AlterField(
14
            model_name='oauth2authorize',
15
            name='creation_date',
16
            field=models.DateTimeField(auto_now_add=True),
17
        ),
18
        migrations.AlterField(
19
            model_name='oauth2tempfile',
20
            name='creation_date',
21
            field=models.DateTimeField(auto_now_add=True),
22
        ),
23
    ]
fargo/oauth2/migrations/0005_auto_20180331_1532.py
1
# Generated by Django 1.11.11 on 2018-03-31 13:32
2

  
3
from django.db import migrations
4

  
5

  
6
class Migration(migrations.Migration):
7

  
8
    dependencies = [
9
        ('oauth2', '0004_auto_20180326_1330'),
10
    ]
11

  
12
    operations = [
13
        migrations.AlterModelOptions(
14
            name='oauth2authorize',
15
            options={
16
                'ordering': ('creation_date',),
17
                'verbose_name': 'OAUTH2 authorization',
18
                'verbose_name_plural': 'OAUTH2 authorizations',
19
            },
20
        ),
21
        migrations.AlterModelOptions(
22
            name='oauth2client',
23
            options={
24
                'ordering': ('client_name',),
25
                'verbose_name': 'OAUTH2 client',
26
                'verbose_name_plural': 'OAUTH2 clients',
27
            },
28
        ),
29
        migrations.AlterModelOptions(
30
            name='oauth2tempfile',
31
            options={
32
                'ordering': ('creation_date',),
33
                'verbose_name': 'OAUTH2 temporary file',
34
                'verbose_name_plural': 'OAUTH2 temporary files',
35
            },
36
        ),
37
    ]
fargo/oauth2/models.py
1
# fargo - document box
2
# Copyright (C) 2016-2019  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import datetime
18
import uuid
19

  
20
from django.conf import settings
21
from django.core.exceptions import ValidationError
22
from django.core.validators import URLValidator
23
from django.db import models
24
from django.db.models.query import QuerySet
25
from django.utils.encoding import python_2_unicode_compatible
26
from django.utils.timezone import now
27
from django.utils.translation import ugettext_lazy as _
28

  
29
from fargo.fargo.models import Document, UserDocument
30

  
31

  
32
def generate_uuid():
33
    return uuid.uuid4().hex
34

  
35

  
36
def validate_https_url(data):
37
    errors = []
38
    data = data.strip()
39
    if not data:
40
        return
41
    for url in data.split():
42
        try:
43
            URLValidator(schemes=['http', 'https'])(url)
44
        except ValidationError as e:
45
            errors.append(e)
46
    if errors:
47
        raise ValidationError(errors)
48

  
49

  
50
@python_2_unicode_compatible
51
class OAuth2Client(models.Model):
52
    client_name = models.CharField(max_length=255)
53
    redirect_uris = models.TextField(verbose_name=_('redirect URIs'), validators=[validate_https_url])
54
    client_id = models.CharField(max_length=255, default=generate_uuid)
55
    client_secret = models.CharField(max_length=255, default=generate_uuid)
56

  
57
    def __repr__(self):
58
        return 'OAuth2Client name: %s with id: %s' % (self.client_name, self.client_id)
59

  
60
    def get_redirect_uris(self):
61
        return self.redirect_uris.split()
62

  
63
    def check_redirect_uri(self, redirect_uri):
64
        return redirect_uri in self.redirect_uris.strip().split()
65

  
66
    def __str__(self):
67
        return self.client_name
68

  
69
    class Meta:
70
        ordering = ('client_name',)
71
        verbose_name = _('OAUTH2 client')
72
        verbose_name_plural = _('OAUTH2 clients')
73

  
74

  
75
class CleanupQuerySet(QuerySet):
76
    def cleanup(self, n=None):
77
        n = n or now()
78
        threshold = n - datetime.timedelta(seconds=2 * self.model.get_lifetime())
79
        self.filter(creation_date__lt=threshold).delete()
80

  
81

  
82
class OAuth2Authorize(models.Model):
83
    client = models.ForeignKey(OAuth2Client, on_delete=models.CASCADE)
84
    user_document = models.ForeignKey(UserDocument, on_delete=models.CASCADE)
85
    access_token = models.CharField(max_length=255, default=generate_uuid)
86
    code = models.CharField(max_length=255, default=generate_uuid)
87
    creation_date = models.DateTimeField(auto_now_add=True)
88

  
89
    objects = CleanupQuerySet.as_manager()
90

  
91
    class Meta:
92
        ordering = ('creation_date',)
93
        verbose_name = _('OAUTH2 authorization')
94
        verbose_name_plural = _('OAUTH2 authorizations')
95

  
96
    @classmethod
97
    def get_lifetime(cls):
98
        return max(settings.FARGO_CODE_LIFETIME, settings.FARGO_ACCESS_TOKEN_LIFETIME)
99

  
100
    def __repr__(self):
101
        return 'OAuth2Authorize for document %r' % self.user_document
102

  
103

  
104
class OAuth2TempFile(models.Model):
105
    uuid = models.CharField(max_length=32, default=generate_uuid, primary_key=True)
106
    client = models.ForeignKey(OAuth2Client, on_delete=models.CASCADE)
107
    document = models.ForeignKey(Document, related_name='oauth2_tempfiles', on_delete=models.CASCADE)
108
    filename = models.CharField(max_length=512)
109
    creation_date = models.DateTimeField(auto_now_add=True)
110

  
111
    objects = CleanupQuerySet.as_manager()
112

  
113
    @classmethod
114
    def get_lifetime(cls):
115
        return settings.FARGO_OAUTH2_TEMPFILE_LIFETIME
116

  
117
    class Meta:
118
        ordering = ('creation_date',)
119
        verbose_name = _('OAUTH2 temporary file')
120
        verbose_name_plural = _('OAUTH2 temporary files')
fargo/oauth2/urls.py
1
# fargo - document box
2
# Copyright (C) 2016-2019  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from django.conf.urls import url
18

  
19
from .views import (
20
    authorize_get_document,
21
    authorize_put_document,
22
    download_put_document,
23
    get_document,
24
    get_document_token,
25
    put_document,
26
)
27

  
28
urlpatterns = [
29
    url(r'get-document/authorize', authorize_get_document, name='oauth2-authorize'),
30
    url(r'get-document/token', get_document_token, name='oauth2-get-token'),
31
    url(r'get-document/', get_document, name='oauth2-get-document'),
32
    url(r'put-document/$', put_document, name='oauth2-put-document'),
33
    url(r'put-document/(?P<pk>\w+)/authorize/', authorize_put_document, name='oauth2-put-document-authorize'),
34
    url(r'put-document/(?P<pk>\w+)/download/', download_put_document, name='oauth2-put-document-download'),
35
]
fargo/oauth2/utils.py
1
# fargo - document box
2
# Copyright (C) 2016-2019  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import cgi
18

  
19
from django.conf import settings
20
from django.utils import six
21
from django.utils.http import unquote
22
from django.utils.timezone import now
23

  
24
from .models import OAuth2Authorize
25

  
26

  
27
def authenticate_bearer(request):
28
    authorization = request.META.get('HTTP_AUTHORIZATION')
29
    if not authorization:
30
        return False
31
    splitted = authorization.split()
32
    if len(splitted) < 2:
33
        return False
34
    if splitted[0] != 'Bearer':
35
        return False
36
    token = splitted[1]
37
    try:
38
        authorize = OAuth2Authorize.objects.get(access_token=token)
39
        if (now() - authorize.creation_date).total_seconds() > settings.FARGO_ACCESS_TOKEN_LIFETIME:
40
            return False
41
        return authorize
42
    except OAuth2Authorize.DoesNotExist:
43
        return False
44

  
45

  
46
def get_content_disposition_value(request):
47
    if 'HTTP_CONTENT_DISPOSITION' not in request.META:
48
        return None, 'missing content-disposition header'
49
    content_header = request.META['HTTP_CONTENT_DISPOSITION']
50
    disposition_type, filename = cgi.parse_header(content_header)
51
    if disposition_type != 'attachment':
52
        return None, 'wrong disposition type: attachment expected'
53
    if 'filename*' in filename:
54
        encode, country, name = filename['filename*'].split("'")
55
        return (unquote(name, encode), None)
56
    elif 'filename' in filename:
57
        return filename['filename'], None
58
    else:
59
        # no filename in header
60
        return None, 'missing filename(*) parameter in header'
fargo/oauth2/views.py
1
# fargo - document box
2
# Copyright (C) 2016-2019  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import logging
18

  
19
from django.conf import settings
20
from django.contrib.auth.decorators import login_required
21
from django.core.files.base import ContentFile
22
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseRedirect
23
from django.shortcuts import get_object_or_404
24
from django.urls import reverse
25
from django.utils.http import quote
26
from django.utils.timezone import now
27
from django.utils.translation import ugettext as _
28
from django.views.decorators.csrf import csrf_exempt
29
from django.views.generic import FormView, TemplateView, View
30
from rest_framework.response import Response
31
from rest_framework.views import APIView
32

  
33
from fargo.fargo.models import Document, UserDocument
34
from fargo.utils import make_url
35

  
36
from .authentication import FargoOAUTH2Authentication
37
from .forms import OAuth2AuthorizeForm
38
from .models import OAuth2Authorize, OAuth2Client, OAuth2TempFile
39
from .utils import authenticate_bearer, get_content_disposition_value
40

  
41
logger = logging.getLogger(__name__)
42

  
43

  
44
class OAuth2Exception(Exception):
45
    pass
46

  
47

  
48
class OAUTH2APIViewMixin(APIView):
49
    http_method_names = ['post']
50
    authentication_classes = (FargoOAUTH2Authentication,)
51

  
52
    @csrf_exempt
53
    def dispatch(self, request, *args, **kwargs):
54
        return super().dispatch(request, *args, **kwargs)
55

  
56

  
57
class OAuth2AuthorizeView(FormView):
58
    template_name = 'fargo/oauth2/authorize.html'
59
    form_class = OAuth2AuthorizeForm
60
    success_url = '/'
61

  
62
    def redirect(self, **kwargs):
63
        '''Return to requester'''
64
        return HttpResponseRedirect(make_url(self.redirect_uri, **kwargs))
65

  
66
    def dispatch(self, request):
67
        self.redirect_uri = request.GET.get('redirect_uri')
68
        if not self.redirect_uri:
69
            return HttpResponseBadRequest('missing redirect_uri parameter')
70

  
71
        client_id = request.GET.get('client_id')
72
        response_type = request.GET.get('response_type')
73
        if not client_id or not response_type:
74
            return self.redirect(error='invalid_request')
75
        if response_type != 'code':
76
            return self.redirect(error='unsupported_response_type')
77
        try:
78
            self.client = OAuth2Client.objects.get(client_id=client_id)
79
            if not self.client.check_redirect_uri(self.redirect_uri):
80
                return self.redirect(error='invalid_redirect_uri')
81
        except OAuth2Client.DoesNotExist:
82
            return self.redirect(error='unauthorized_client')
83
        self.state = request.GET.get('state', None)
84
        return super().dispatch(request)
85

  
86
    def post(self, request):
87
        if 'cancel' in request.POST:
88
            return self.redirect(error='access_denied')
89
        return super().post(request)
90

  
91
    def get_form_kwargs(self):
92
        kwargs = super().get_form_kwargs()
93
        kwargs['user'] = self.request.user
94
        return kwargs
95

  
96
    def form_valid(self, form):
97
        document = form.cleaned_data['document']
98
        authorization = OAuth2Authorize.objects.create(client=self.client, user_document=document)
99
        logger.info(
100
            'user %s authorized client "%s" to get document "%s" (%s) with code "%s"',
101
            self.request.user,
102
            self.client,
103
            document,
104
            document.pk,
105
            authorization.code,
106
        )
107
        return self.redirect(code=authorization.code, state=self.state)
108

  
109
    def get_context_data(self, **kwargs):
110
        kwargs['oauth2_client'] = self.client
111
        return super().get_context_data(**kwargs)
112

  
113

  
114
authorize_get_document = login_required(OAuth2AuthorizeView.as_view())
115

  
116

  
117
class GetDocumentTokenView(OAUTH2APIViewMixin):
118
    def error(self, error, description=None):
119
        data = {
120
            'error': error,
121
        }
122
        if description:
123
            data['error_description'] = description
124
        return Response(data, status=400)
125

  
126
    def post(self, request):
127
        if request.data['grant_type'] != 'authorization_code':
128
            return self.error('unsupported_grant_type')
129

  
130
        try:
131
            authorize = OAuth2Authorize.objects.get(code=request.data['code'])
132
        except OAuth2Authorize.DoesNotExist:
133
            return self.error('invalid_grant', 'code is unknown')
134

  
135
        if (now() - authorize.creation_date).total_seconds() > settings.FARGO_CODE_LIFETIME:
136
            return self.error('invalid_grant', 'code is expired')
137
        logger.info(
138
            'client "%s" resolved code "%s" to access token "%s"',
139
            request.user.oauth2_client,
140
            authorize.code,
141
            authorize.access_token,
142
        )
143
        return Response(
144
            {'access_token': authorize.access_token, 'expires': settings.FARGO_ACCESS_TOKEN_LIFETIME}
145
        )
146

  
147

  
148
get_document_token = GetDocumentTokenView.as_view()
149

  
150

  
151
def document_response(user_document):
152
    response = HttpResponse(
153
        content=user_document.document.content.chunks(), status=200, content_type='application/octet-stream'
154
    )
155

  
156
    filename = user_document.filename
157
    ascii_filename = filename.encode('ascii', 'replace').decode()
158
    percent_encoded_filename = quote(filename.encode('utf8'), safe='')
159
    response['Content-Disposition'] = 'attachment; filename="%s"; filename*=UTF-8\'\'%s' % (
160
        ascii_filename,
161
        percent_encoded_filename,
162
    )
163
    return response
164

  
165

  
166
def get_document(request):
167
    oauth_authorize = authenticate_bearer(request)
168
    if not oauth_authorize:
169
        return HttpResponseBadRequest('http bearer authentication failed: invalid authorization header')
170

  
171
    user_document = oauth_authorize.user_document
172
    logger.info(
173
        'client "%s" retrieved document "%s" (%s) with access token "%s"',
174
        oauth_authorize.client,
175
        user_document,
176
        user_document.pk,
177
        oauth_authorize.access_token,
178
    )
179
    return document_response(user_document)
180

  
181

  
182
class PutDocumentAPIView(OAUTH2APIViewMixin):
183
    def post(self, request, *args, **kwargs):
184
        filename, error = get_content_disposition_value(request)
185
        if error:
186
            return HttpResponseBadRequest(error)
187

  
188
        f = ContentFile(request.body, name=filename)
189
        document = Document.objects.get_by_file(f)
190
        oauth2_document = OAuth2TempFile.objects.create(
191
            client=request.user.oauth2_client, document=document, filename=filename
192
        )
193
        uri = reverse('oauth2-put-document-authorize', args=[oauth2_document.pk])
194

  
195
        response = Response()
196
        response['Location'] = uri
197
        logger.info(
198
            'client "%s" uploaded document "%s" (%s)',
199
            request.user.oauth2_client,
200
            filename,
201
            oauth2_document.pk,
202
        )
203
        return response
204

  
205

  
206
put_document = PutDocumentAPIView.as_view()
207

  
208

  
209
class OAuth2AuthorizePutView(TemplateView):
210
    template_name = 'fargo/oauth2/confirm.html'
211

  
212
    def redirect(self, **kwargs):
213
        '''Return to requester'''
214
        return HttpResponseRedirect(make_url(self.redirect_uri, **kwargs))
215

  
216
    def dispatch(self, request, *args, **kwargs):
217
        self.redirect_uri = request.GET.get('redirect_uri', '')
218
        if not self.redirect_uri:
219
            return HttpResponseBadRequest('missing redirect_uri parameter')
220
        self.oauth2_document = OAuth2TempFile.objects.filter(pk=kwargs['pk']).first()
221
        return super().dispatch(request)
222

  
223
    def get_context_data(self, **kwargs):
224
        if self.oauth2_document:
225
            kwargs['oauth2_document'] = self.oauth2_document
226
            kwargs['filename'] = self.oauth2_document.filename
227
            kwargs['thumbnail_image'] = self.oauth2_document.document.thumbnail_image
228
            kwargs['oauth2_client'] = self.oauth2_document.client
229
            kwargs['download_url'] = reverse(
230
                'oauth2-put-document-download', kwargs={'pk': self.oauth2_document.pk}
231
            )
232
            # verify if document already exists
233
            if not UserDocument.objects.filter(
234
                user=self.request.user, document=self.oauth2_document.document
235
            ).exists():
236
                kwargs['error_message'] = ''
237
            else:
238
                kwargs['error_message'] = _('This document is already in your portfolio')
239
                kwargs['redirect_uri'] = self.request.GET['redirect_uri']
240
        else:
241
            kwargs['error_message'] = _('The document has not been uploaded')
242
            kwargs['redirect_uri'] = self.request.GET['redirect_uri']
243
        return super().get_context_data(**kwargs)
244

  
245
    def post(self, request):
246
        if not self.oauth2_document:
247
            return self.get(request)
248

  
249
        try:
250
            if 'cancel' in request.POST:
251
                return self.redirect(error='access_denied')
252

  
253
            UserDocument.objects.create(
254
                user=request.user,
255
                document=self.oauth2_document.document,
256
                filename=self.oauth2_document.filename,
257
            )
258
            logger.info(
259
                'user %s accepted document "%s" (%s) from client "%s"',
260
                request.user,
261
                self.oauth2_document.filename,
262
                self.oauth2_document.pk,
263
                self.oauth2_document.client,
264
            )
265
            return self.redirect()
266
        finally:
267
            self.oauth2_document.delete()
268

  
269

  
270
authorize_put_document = login_required(OAuth2AuthorizePutView.as_view())
271

  
272

  
273
class DownloadPutDocument(View):
274
    def get(self, request, *args, **kwargs):
275
        oauth2_document = get_object_or_404(OAuth2TempFile, pk=kwargs['pk'])
276
        return document_response(oauth2_document)
277

  
278

  
279
download_put_document = login_required(DownloadPutDocument.as_view())
fargo/settings.py
55 55
    'django_tables2',
56 56
    'gadjo',
57 57
    'fargo.fargo',
58
    'rest_framework',
59
    'fargo.oauth2',
60 58
    'sorl.thumbnail',
61 59
)
62 60

  
......
258 256

  
259 257
FARGO_CODE_LIFETIME = 300
260 258
FARGO_ACCESS_TOKEN_LIFETIME = 3600
261
FARGO_OAUTH2_TEMPFILE_LIFETIME = 86400
262 259

  
263 260
local_settings_file = os.environ.get(
264 261
    'FARGO_SETTINGS_FILE', os.path.join(os.path.dirname(__file__), 'local_settings.py')
fargo/templates/fargo/oauth2/authorize.html
1
{% extends "fargo/base.html" %}
2
{% load i18n %}
3

  
4
{% block content %}
5
<div id="fargo-oauth2-authorize">
6
  {% block form-intro %}
7
    {% blocktrans %}
8
      <p>The service {{ oauth2_client }} want to get one of your documents.</p>
9
    {% endblocktrans %}
10
  {% endblock %}
11
  {% block form %}
12
  <form method="post" enctype="multipart/form-data">
13
    {% csrf_token %}
14
    {{ form.as_p  }}
15
    <div class="buttons">
16
      <button name="submit">{% trans "Choose" %}</button>
17
      <button name="cancel">{% trans "Cancel" %}</button>
18
    </div>
19
  </form>
20
  {% endblock %}
21
</div>
22
{% endblock %}
fargo/templates/fargo/oauth2/confirm.html
1
{% extends "fargo/base.html" %}
2
{% load i18n %}
3

  
4
{% block content %}
5
<div id="fargo-oauth2-confirm">
6
  {% if oauth2_document %}
7
    {% block form-intro %}
8
      <p>
9
         {% blocktrans %}
10
The service {{ oauth2_client }} want to add the document "<a href="{{ download_url }}"><em class="filename">{{ filename }}</em></a>" to your portfolio.
11
         {% endblocktrans %}
12
      </p>
13
      {% if thumbnail %}<p class="fargo-thumbnail"><img src="{{ thumbnail.src  }}" height="{{ thumbnail.height }}" width="{{ thumbnail.width }}"/></p>{% endif %}
14
    {% endblock %}
15
  {% endif %}
16
  {% if error_message %}
17
    {% block error-message %}
18
      <p>{% trans error_message %}</p>
19
    {% endblock %}
20
  {% endif %}
21
  {% block form %}
22
    <form id="send-file" method="post">
23
      {% csrf_token %}
24
      <div class="buttons">
25
        {% if not error_message %}
26
          <button name="submit">{% trans "Allow" %}</button>
27
        {% endif %}
28
        <button name="cancel">{% trans "Cancel" %}</button>
29
      </div>
30
    </form>
31
  {% endblock %}
32
</div>
33
{% endblock %}
fargo/urls.py
55 55
    url(r'^api/documents/push/$', push_document, name='fargo-api-push-document'),
56 56
    url(r'^api/documents/recently-added/$', recent_documents),
57 57
    url(r'^api/', include(router.urls)),
58
    url(r'^api/', include('fargo.oauth2.urls')),
59 58
]
60 59

  
61 60
if settings.DEBUG and 'debug_toolbar' in settings.INSTALLED_APPS:
tests/conftest.py
100 100

  
101 101
@pytest.fixture
102 102
def document():
103
    with open('tests/test_oauth2.txt', 'rb') as f:
104
        content = ContentFile(f.read(), 'test_oauth2.txt')
103
    with open('tests/test_file.txt', 'rb') as f:
104
        content = ContentFile(f.read(), 'test_file.txt')
105 105

  
106 106
    return Document.objects.get_by_file(content)
107 107

  
tests/test_commands.py
21 21
from django.core.management import call_command
22 22

  
23 23
from fargo.fargo.models import Document, UserDocument
24
from fargo.oauth2.models import OAuth2Client, OAuth2TempFile
25 24

  
26 25

  
27 26
def test_cleanup(freezer, john_doe):
28 27
    start = freezer()
29 28

  
30
    client = OAuth2Client.objects.create(client_name='c', redirect_uris='')
31

  
32 29
    foo = Document.objects.create(content=ContentFile(b'foo', name='foo.txt'))
33
    bar = Document.objects.create(content=ContentFile(b'bar', name='bar.txt'))
34 30
    UserDocument.objects.create(user=john_doe, document=foo, filename='foo.txt', title='', description='')
35
    OAuth2TempFile.objects.create(document=bar, client=client, filename='bar.txt')
36 31

  
37 32
    call_command('fargo-cleanup')
38 33

  
39 34
    assert UserDocument.objects.all().count()
40
    assert OAuth2TempFile.objects.all().count()
41
    assert Document.objects.all().count() == 2
35
    assert Document.objects.all().count() == 1
42 36

  
43 37
    User.objects.all().delete()
44 38

  
45 39
    assert not UserDocument.objects.all().count()
46
    assert Document.objects.all().count() == 2
47

  
48
    call_command('fargo-cleanup')
49

  
50
    assert Document.objects.all().count() == 2
40
    assert Document.objects.all().count() == 1
51 41

  
52
    freezer.move_to(start + datetime.timedelta(seconds=120))
53 42
    call_command('fargo-cleanup')
54 43

  
55 44
    assert Document.objects.all().count() == 1
......
58 47

  
59 48
    call_command('fargo-cleanup')
60 49

  
61
    assert not OAuth2TempFile.objects.count()
62
    assert Document.objects.count()
63

  
64
    call_command('fargo-cleanup')
65

  
66 50
    assert not Document.objects.count()
tests/test_file.txt
1
Lorem ipsum dolor sit amet, atqui animal constituto sit no, pri liber mandamus
2
ea, usu no duis etiam copiosae. Ius liber scripserit at, nam nisl nonumes ne.
3
Ut vidit clita possim eum, eos eu melius perfecto. Ne ius intellegam
4
reformidans, pri repudiare conceptam definitiones cu, duo tota bonorum no.
5
Lorem omnesque principes in ius, facilis erroribus cu usu. Eum liber homero
6
qualisque id, cu pri illum consetetur.
tests/test_oauth2.py
1
# fargo - document box
2
# Copyright (C) 2016-2019  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import json
18
import os
19
from unittest import mock
20

  
21
import pytest
22
from django.core.management import call_command
23
from django.urls import reverse
24
from django.utils.http import quote, urlencode
25
from django.utils.six.moves.urllib import parse as urlparse
26
from test_manager import login
27

  
28
from fargo.fargo.models import UserDocument
29
from fargo.oauth2.models import OAuth2Authorize, OAuth2Client, OAuth2TempFile
30

  
31
pytestmark = pytest.mark.django_db
32

  
33

  
34
class FakedResponse(mock.Mock):
35
    def json(self):
36
        return json.loads(self.content)
37

  
38

  
39
@pytest.fixture
40
def oauth2_client():
41
    return OAuth2Client.objects.create(
42
        client_name='test_oauth2',
43
        client_id='client-id',
44
        client_secret='client-secret',
45
        redirect_uris='https://example.net/document https://doc.example.net/ https://example.com',
46
    )
47

  
48

  
49
def assert_error_redirect(url, error):
50
    assert urlparse.urlparse(url).query == 'error=%s' % error
51

  
52

  
53
def test_get_document_oauth2(app, john_doe, oauth2_client, user_doc):
54
    login(app, user=john_doe)
55
    url = reverse('oauth2-authorize')
56
    params = {'client_secret': oauth2_client.client_secret, 'response_type': 'code', 'state': 'achipeachope'}
57
    # test missing redirect_uri
58
    resp = app.get(url, params={}, status=400)
59
    assert resp.text == 'missing redirect_uri parameter'
60
    # test missing client id
61
    params['redirect_uri'] = 'https://toto.example.com'
62
    resp = app.get(url, params=params, status=302)
63
    assert_error_redirect(resp.url, 'invalid_request')
64
    # test invalid response type
65
    params['client_id'] = oauth2_client.client_id
66
    params['response_type'] = 'token'
67
    resp = app.get(url, params=params, status=302)
68
    assert_error_redirect(resp.url, 'unsupported_response_type')
69
    # test invalid redirect uri
70
    params['response_type'] = 'code'
71
    resp = app.get(url, params=params, status=302)
72
    assert_error_redirect(resp.url, 'invalid_redirect_uri')
73

  
74
    params['redirect_uri'] = 'https://example.com'
75
    resp = app.get(url, params=params)
76

  
77
    assert resp.status_code == 200
78
    assert len(resp.form['document'].options) == 2
79
    options = resp.form['document'].options
80
    assert 'éléphant.txt' in options[1]
81

  
82
    # select the second document 'éléphant.txt'
83
    resp.form['document'].select(options[1][0])
84
    resp = resp.form.submit()
85
    # check that the authorization has been registered for the user document
86
    assert len(OAuth2Authorize.objects.filter(user_document__user=john_doe)) == 1
87
    auth = OAuth2Authorize.objects.filter(user_document__user=john_doe)[0]
88
    assert resp.status_code == 302
89
    query = urlparse.urlparse(resp.location).query
90
    assert [auth.code] == urlparse.parse_qs(query)['code']
91
    assert ['achipeachope'] == urlparse.parse_qs(query)['state']
92

  
93
    params.pop('response_type')
94
    params.pop('state')
95
    params['grant_type'] = 'authorization_code'
96
    params['code'] = auth.code
97

  
98
    url = reverse('oauth2-get-token')
99
    app.authorization = ('Basic', (oauth2_client.client_id, oauth2_client.client_secret))
100
    resp = app.post(url, params=params, status=200)
101
    assert 'access_token' in resp.json
102
    assert 'expires' in resp.json
103
    assert resp.json['access_token'] == auth.access_token
104

  
105
    url = reverse('oauth2-get-document')
106
    app.authorization = ('Bearer', str(auth.access_token))
107
    resp = app.get(url, status=200)
108

  
109
    assert resp.content_type == 'application/octet-stream'
110
    assert 'Content-disposition' in resp.headers
111
    content_disposition = resp.content_disposition.replace(' ', '').split(';')
112
    assert content_disposition[0] == 'attachment'
113
    assert content_disposition[1] == 'filename="?l?phant.txt"'
114
    assert content_disposition[2] == 'filename*=UTF-8\'\'%C3%A9l%C3%A9phant.txt'
115

  
116

  
117
def test_put_document(app, john_doe, oauth2_client):
118
    login(app, user=john_doe)
119
    with open('tests/test_oauth2.txt', 'rb') as f:
120
        data = f.read()
121

  
122
    url = reverse('oauth2-put-document')
123
    resp = app.post(url, params=data, status=401)
124

  
125
    app.authorization = ('Basic', (str(oauth2_client.client_id), str(oauth2_client.client_secret)))
126
    resp = app.post(url, params=data, status=400)
127
    assert 'missing content-disposition header' in resp.text
128

  
129
    filename = 'éléphant.txt'
130
    percent_encode_filename = quote(filename, safe='')
131
    headers = {
132
        'Content-disposition': 'attachment; filename="%s"; filename*=UTF-8\'\'%s'
133
        % (filename, percent_encode_filename)
134
    }
135

  
136
    assert len(OAuth2TempFile.objects.all()) == 0
137

  
138
    resp = app.post(url, params=data, headers=headers, status=200)
139
    # test that we can still push the same document
140
    resp = app.post(url, params=data, headers=headers, status=200)
141

  
142
    assert len(OAuth2TempFile.objects.all()) == 2
143

  
144
    doc = OAuth2TempFile.objects.latest('creation_date')
145
    location = reverse('oauth2-put-document-authorize', kwargs={'pk': doc.pk})
146
    assert location in resp.location
147

  
148
    app.authorization = None
149
    url = location + '?%s' % urlencode({'redirect_uri': 'https://example.com'})
150
    resp = app.get(url, status=200)
151

  
152
    assert OAuth2TempFile.objects.count() == 2
153
    assert UserDocument.objects.count() == 0
154

  
155
    resp = resp.form.submit()
156
    assert resp.status_code == 302
157
    assert resp.location == 'https://example.com'
158

  
159
    assert OAuth2TempFile.objects.count() == 1
160
    assert UserDocument.objects.count() == 1
161
    assert OAuth2TempFile.objects.get().document == UserDocument.objects.get().document
162
    assert UserDocument.objects.filter(user=john_doe, document=doc.document, filename='éléphant.txt').exists()
163

  
164

  
165
def test_confirm_put_document_file_exception(app, oauth2_client, john_doe, user_doc):
166
    login(app, user=john_doe)
167
    oauth_tmp_file = OAuth2TempFile.objects.create(
168
        client=oauth2_client, document=user_doc.document, filename=user_doc.filename
169
    )
170

  
171
    url = reverse('oauth2-put-document-authorize', kwargs={'pk': 'fakemofo'})
172
    url += '?%s' % urlencode({'redirect_uri': 'https://example.com'})
173
    resp = app.get(url)
174
    assert 'The document has not been uploaded' in resp.text
175

  
176
    url = reverse('oauth2-put-document-authorize', kwargs={'pk': oauth_tmp_file.pk})
177
    url += '?%s' % urlencode({'redirect_uri': 'https://example.com'})
178
    resp = app.get(url)
179
    assert 'This document is already in your portfolio' in resp.text
180

  
181

  
182
@mock.patch('fargo.oauth2.authentication.requests.post')
183
def test_idp_authentication(mocked_post, settings, app, oauth2_client, john_doe, user_doc):
184
    login(app, user=john_doe)
185
    url = reverse('oauth2-authorize')
186
    params = {
187
        'client_id': oauth2_client.client_id,
188
        'client_secret': 'fake',
189
        'response_type': 'code',
190
        'state': 'achipeachope',
191
        'redirect': 'https://example.com/',
192
    }
193
    params['redirect_uri'] = 'https://example.com'
194
    resp = app.get(url, params=params)
195
    options = resp.form['document'].options
196
    assert 'éléphant.txt' in options[1]
197
    resp.form['document'].select(options[1][0])
198
    resp = resp.form.submit()
199
    auth = OAuth2Authorize.objects.filter(user_document__user=john_doe)[0]
200
    params.pop('response_type')
201
    params.pop('state')
202
    params['grant_type'] = 'authorization_code'
203
    params['code'] = auth.code
204

  
205
    url = reverse('oauth2-get-token')
206
    # when remote remote idp not set
207
    app.authorization = ('Basic', ('client-id', 'fake'))
208
    resp = app.post(url, params=params, status=401)
209
    resp.json['detail'] == 'Invalid client_id/client_secret.'
210
    # when remote idp fails to authenticate rp
211
    settings.FARGO_IDP_URL = 'https://idp.example.org'
212
    response = {"result": 0, "errors": ["Invalid username/password."]}
213
    mocked_post.return_value = FakedResponse(content=json.dumps(response))
214
    resp = app.post(url, params=params, status=401)
215
    resp.json['detail'] == 'Invalid client_id/client_secret.'
216
    # when remote idp authenticates rp
217
    response = {"result": 1, "errors": []}
218
    mocked_post.return_value = FakedResponse(content=json.dumps(response))
219
    resp = app.post(url, params=params, status=200)
220
    assert resp.json['access_token'] == auth.access_token
221
    url = reverse('oauth2-get-document')
222
    app.authorization = ('Bearer', str(auth.access_token))
223
    resp = app.get(url, status=200)
224

  
225

  
226
def test_command_create_client(db):
227
    call_command('oauth2-create-client', 'test', 'https://example.com/')
228
    client = OAuth2Client.objects.get()
229
    assert client.client_name == 'test'
230
    assert client.redirect_uris == 'https://example.com/'
231
    assert client.client_id
232
    assert client.client_secret
233

  
234
    OAuth2Client.objects.all().delete()
235

  
236
    call_command(
237
        'oauth2-create-client', 'test', 'https://example.com/', '--client-id=wtf', '--client-secret=whocares'
238
    )
239
    client = OAuth2Client.objects.get()
240
    assert client.client_name == 'test'
241
    assert client.redirect_uris == 'https://example.com/'
242
    assert client.client_id == 'wtf'
243
    assert client.client_secret == 'whocares'
244

  
245

  
246
def test_command_put_document(db, capsys, app, john_doe):
247
    call_command('oauth2-create-client', 'test', 'https://example.com/')
248
    client = OAuth2Client.objects.get()
249
    path = os.path.join(os.path.dirname(__file__), 'pdf-sample.pdf')
250
    redirect_uri = 'https://example.com/'
251
    call_command('oauth2-put-document', '--client-id=%s' % client.pk, redirect_uri, path)
252
    out, err = capsys.readouterr()
253
    assert err == ''
254
    url = out.strip()
255
    response = app.get(url).follow()
256
    response.form.set('username', john_doe.username)
257
    response.form.set('password', john_doe.username)
258
    response = response.form.submit().follow()
259
    assert 'pdf-sample.pdf' in response
260
    temp_file = OAuth2TempFile.objects.get()
261
    assert temp_file.uuid in response
262
    response = response.form.submit('accept')
263
    assert response['Location'] == redirect_uri
264
    assert UserDocument.objects.filter(user=john_doe, document=temp_file.document).exists()
265
    assert OAuth2TempFile.objects.count() == 0
tests/test_oauth2.txt
1
Poème hymne à la beauté du recueil les fleurs du mal de Charles Baudelaire
2

  
3
Viens-tu du ciel profond ou sors-tu de l'abîme,
4
Ô Beauté ! ton regard, infernal et divin,
5
Verse confusément le bienfait et le crime,
6
Et l'on peut pour cela te comparer au vin.
7

  
8
Tu contiens dans ton oeil le couchant et l'aurore ;
9
Tu répands des parfums comme un soir orageux ;
10
Tes baisers sont un philtre et ta bouche une amphore
11
Qui font le héros lâche et l'enfant courageux.
12

  
13
Sors-tu du gouffre noir ou descends-tu des astres ?
14
Le Destin charmé suit tes jupons comme un chien ;
15
Tu sèmes au hasard la joie et les désastres,
16
Et tu gouvernes tout et ne réponds de rien.
17

  
18
Tu marches sur des morts, Beauté, dont tu te moques ;
19
De tes bijoux l'Horreur n'est pas le moins charmant,
20
Et le Meurtre, parmi tes plus chères breloques,
21
Sur ton ventre orgueilleux danse amoureusement.
22

  
23
L'éphémère ébloui vole vers toi, chandelle,
24
Crépite, flambe et dit : Bénissons ce flambeau !
25
L'amoureux pantelant incliné sur sa belle
26
A l'air d'un moribond caressant son tombeau.
27

  
28
Que tu viennes du ciel ou de l'enfer, qu'importe,
29
Ô Beauté ! monstre énorme, effrayant, ingénu !
30
Si ton oeil, ton souris, ton pied, m'ouvrent la porte
31
D'un Infini que j'aime et n'ai jamais connu ?
32

  
33
De Satan ou de Dieu, qu'importe ? Ange ou Sirène,
34
Qu'importe, si tu rends, - fée aux yeux de velours,
35
Rythme, parfum, lueur, ô mon unique reine ! -
36
L'univers moins hideux et les instants moins lourds ?
37
-