Projet

Général

Profil

0001-idp_oidc-make-sub-depend-on-user-s-profile-choice-du.patch

Paul Marillonnet, 14 février 2022 15:26

Télécharger (12,3 ko)

Voir les différences:

Subject: [PATCH] idp_oidc: make sub depend on user's profile choice during
 authz (#58556)

 src/authentic2/app_settings.py                |  4 +++
 src/authentic2_idp_oidc/apps.py               |  4 +--
 .../migrations/0015_oidccode_profile_id.py    | 18 ++++++++++
 src/authentic2_idp_oidc/models.py             |  1 +
 .../authentic2_idp_oidc/authorization.html    | 13 +++++++
 src/authentic2_idp_oidc/utils.py              | 35 +++++++++++--------
 src/authentic2_idp_oidc/views.py              | 26 ++++++++++++--
 7 files changed, 83 insertions(+), 18 deletions(-)
 create mode 100644 src/authentic2_idp_oidc/migrations/0015_oidccode_profile_id.py
src/authentic2/app_settings.py
333 333
        default=250,
334 334
        definition='Maximum number of mails to send per period',
335 335
    ),
336
    A2_USER_PROFILE_MANAGEMENT=Setting(
337
        default=False,
338
        definition='Activate user profiles for juridical entity management.',
339
    ),
336 340
)
337 341

  
338 342
app_settings = AppSettings(default_settings)
src/authentic2_idp_oidc/apps.py
88 88
        sub = smart_bytes(view.kwargs[lookup_url_kwarg])
89 89
        decrypted = utils.reverse_pairwise_sub(client, sub)
90 90
        if decrypted:
91
            view.kwargs[lookup_url_kwarg] = uuid.UUID(bytes=decrypted).hex
91
            view.kwargs[lookup_url_kwarg] = uuid.UUID(bytes=decrypted.split(b'#')[0]).hex
92 92

  
93 93
    def a2_hook_api_modify_serializer_after_validation(self, view, serializer):
94 94
        import uuid
......
111 111
        for u in serializer.validated_data['known_uuids']:
112 112
            decrypted = utils.reverse_pairwise_sub(client, smart_bytes(u))
113 113
            if decrypted:
114
                new_known_uuid = uuid.UUID(bytes=decrypted).hex
114
                new_known_uuid = uuid.UUID(bytes=decrypted.split(b'#')[0]).hex
115 115
                new_known_uuids.append(new_known_uuid)
116 116
                uuid_map[new_known_uuid] = u
117 117
            else:
src/authentic2_idp_oidc/migrations/0015_oidccode_profile_id.py
1
# Generated by Django 2.2.24 on 2022-02-14 09:57
2

  
3
from django.db import migrations, models
4

  
5

  
6
class Migration(migrations.Migration):
7

  
8
    dependencies = [
9
        ('authentic2_idp_oidc', '0014_auto_20201126_1812'),
10
    ]
11

  
12
    operations = [
13
        migrations.AddField(
14
            model_name='oidccode',
15
            name='profile_id',
16
            field=models.PositiveIntegerField(null=True, verbose_name='profile identifier'),
17
        ),
18
    ]
src/authentic2_idp_oidc/models.py
288 288
    uuid = models.CharField(max_length=128, verbose_name=_('uuid'), default=generate_uuid)
289 289
    client = models.ForeignKey(to=OIDCClient, verbose_name=_('client'), on_delete=models.CASCADE)
290 290
    user = models.ForeignKey(to=settings.AUTH_USER_MODEL, verbose_name=_('user'), on_delete=models.CASCADE)
291
    profile_id = models.PositiveIntegerField(verbose_name=_('profile identifier'), null=True)
291 292
    scopes = models.TextField(verbose_name=_('scopes'))
292 293
    state = models.TextField(null=True, verbose_name=_('state'))
293 294
    nonce = models.TextField(null=True, verbose_name=_('nonce'))
src/authentic2_idp_oidc/templates/authentic2_idp_oidc/authorization.html
16 16
        {% endfor %}
17 17
      </ul>
18 18
    {% endif %}
19
    {% if needs_profile_validation %}
20
     <p>{% trans "Additionally, you may authenticate as owner of the following juridical entity management profile, which may change the aforementioned information:" %}</p>
21
      <div id="profile-validation-none">
22
        <input type="radio" id="_none" name="profile-validation" value="">
23
        <label for="_none">{% trans "Keep my default user profile." %}</label>
24
      </div>
25
      {% for profile in user.profiles %}
26
        <div id="profile-validation-{{ profile.id }}">
27
          <input type="radio" id="{{ profile.id }}" name="profile-validation" value="{{ profile.id }}">
28
          <label for="{{ profile.slug }}">{% blocktrans with type_name=profile.profile_type.name %}Profile of type {{ type_name }}.{% endblocktrans %}</label>
29
        </div>
30
      {% endfor %}
31
    {% endif %}
19 32
    {% csrf_token %}
20 33
    <p class="a2-oidc-authorization-form--do-not-ask-again">
21 34
      <label for="id_do_not_ask_again"><input id="id_do_not_ask_again" type="checkbox" name="do_not_ask_again" value="on"/><span>{% trans "Do not ask again" %}</span></label>
src/authentic2_idp_oidc/utils.py
121 121
    return urllib.parse.urlparse(url).netloc.split(':')[0]
122 122

  
123 123

  
124
def make_sub(client, user):
124
def make_sub(client, user, profile_id=None):
125 125
    if client.identifier_policy in (client.POLICY_PAIRWISE, client.POLICY_PAIRWISE_REVERSIBLE):
126
        return make_pairwise_sub(client, user)
126
        return make_pairwise_sub(client, user, profile_id=profile_id)
127 127
    elif client.identifier_policy == client.POLICY_UUID:
128 128
        return force_text(user.uuid)
129 129
    elif client.identifier_policy == client.POLICY_EMAIL:
......
132 132
        raise NotImplementedError
133 133

  
134 134

  
135
def make_pairwise_sub(client, user):
135
def make_pairwise_sub(client, user, profile_id=None):
136 136
    '''Make a pairwise sub'''
137 137
    if client.identifier_policy == client.POLICY_PAIRWISE:
138
        return make_pairwise_unreversible_sub(client, user)
138
        return make_pairwise_unreversible_sub(client, user, profile_id=profile_id)
139 139
    elif client.identifier_policy == client.POLICY_PAIRWISE_REVERSIBLE:
140
        return make_pairwise_reversible_sub(client, user)
140
        return make_pairwise_reversible_sub(client, user, profile_id=profile_id)
141 141
    else:
142 142
        raise NotImplementedError('unknown pairwise client.identifier_policy %s' % client.identifier_policy)
143 143

  
144 144

  
145
def make_pairwise_unreversible_sub(client, user):
145
def make_pairwise_unreversible_sub(client, user, profile_id=None):
146 146
    sector_identifier = client.get_sector_identifier()
147 147
    sub = sector_identifier + str(user.uuid) + settings.SECRET_KEY
148
    if profile_id is not None:
149
        sub += str(profile_id)
148 150
    sub = base64.b64encode(hashlib.sha256(sub.encode('utf-8')).digest())
149 151
    return sub.decode('utf-8')
150 152

  
151 153

  
152
def make_pairwise_reversible_sub(client, user):
153
    return make_pairwise_reversible_sub_from_uuid(client, user.uuid)
154
def make_pairwise_reversible_sub(client, user, profile_id=None):
155
    return make_pairwise_reversible_sub_from_uuid(client, user.uuid, profile_id=profile_id)
154 156

  
155 157

  
156
def make_pairwise_reversible_sub_from_uuid(client, user_uuid):
158
def make_pairwise_reversible_sub_from_uuid(client, user_uuid, profile_id=None):
157 159
    try:
158 160
        identifier = uuid.UUID(user_uuid).bytes
159 161
    except ValueError:
160 162
        return None
161 163
    sector_identifier = client.get_sector_identifier()
162
    return crypto.aes_base64url_deterministic_encrypt(
163
        settings.SECRET_KEY.encode('utf-8'), identifier, sector_identifier
164
    ).decode('utf-8')
164
    cipher_args = [
165
        settings.SECRET_KEY.encode('utf-8'),
166
        identifier,
167
        sector_identifier,
168
    ]
169
    if profile_id is not None:
170
        cipher_args[1] += '#%s' % str(profile_id)
171
    return crypto.aes_base64url_deterministic_encrypt(*cipher_args).decode('utf-8')
165 172

  
166 173

  
167 174
def reverse_pairwise_sub(client, sub):
......
184 191
    return str(value)
185 192

  
186 193

  
187
def create_user_info(request, client, user, scope_set, id_token=False):
194
def create_user_info(request, client, user, scope_set, id_token=False, profile_id=None):
188 195
    '''Create user info dictionary'''
189 196
    user_info = {}
190 197
    if 'openid' in scope_set:
191
        user_info['sub'] = make_sub(client, user)
198
        user_info['sub'] = make_sub(client, user, profile_id=profile_id)
192 199
    attributes = get_attributes(
193 200
        {
194 201
            'user': user,
src/authentic2_idp_oidc/views.py
38 38
from authentic2 import app_settings as a2_app_settings
39 39
from authentic2 import hooks
40 40
from authentic2.a2_rbac.models import OrganizationalUnit
41
from authentic2.custom_user.models import Profile
41 42
from authentic2.decorators import setting_enabled
42 43
from authentic2.exponential_retry_timeout import ExponentialRetryTimeout
43 44
from authentic2.utils.misc import last_authentication_event, login_require, make_url, redirect
......
359 360

  
360 361
    iat = now()  # iat = issued at
361 362

  
363
    needs_profile_validation = False
364
    profile = None
365
    if request.user.subprofiles and a2_app_settings.A2_USER_PROFILE_MANAGEMENT:
366
        needs_profile_validation = True
362 367
    if client.authorization_mode != client.AUTHORIZATION_MODE_NONE or 'consent' in prompt:
363 368
        # authorization by user is mandatory, as per local configuration or per explicit request by
364 369
        # the RP
......
381 386
        authorized_scopes = set()
382 387
        for authorization in qs:
383 388
            authorized_scopes |= authorization.scope_set()
384
        if (authorized_scopes & scopes) < scopes:
389
        if (authorized_scopes & scopes) < scopes or needs_profile_validation:
385 390
            if 'none' in prompt:
386 391
                raise ConsentRequired(_('Consent is required but prompt parameter is "none"'))
387 392
            if request.method == 'POST':
393
                if request.POST.get('profile-validation', ''):
394
                    try:
395
                        profile = Profile.objects.get(
396
                            user=request.user,
397
                            slug=request.POST['profile-validation'],
398
                        )
399
                    except Profile.DoesNotExist:
400
                        pass
388 401
                if 'accept' in request.POST:
389 402
                    if 'do_not_ask_again' in request.POST:
390 403
                        pk_to_deletes = []
......
417 430
                    request,
418 431
                    'authentic2_idp_oidc/authorization.html',
419 432
                    {
433
                        'needs_profile_validation': needs_profile_validation,
420 434
                        'client': client,
421 435
                        'scopes': scopes - {'openid'},
422 436
                    },
......
425 439
        code = models.OIDCCode.objects.create(
426 440
            client=client,
427 441
            user=request.user,
442
            profile_id=profile.id if profile else None,
428 443
            scopes=' '.join(scopes),
429 444
            state=state,
430 445
            nonce=nonce,
......
721 736
    ):
722 737
        acr = '1'
723 738
    # prefill id_token with user info
724
    id_token = utils.create_user_info(request, client, oidc_code.user, oidc_code.scope_set(), id_token=True)
739
    id_token = utils.create_user_info(
740
        request,
741
        client,
742
        oidc_code.user,
743
        oidc_code.scope_set(),
744
        id_token=True,
745
        profile_id=oidc_code.profile_id,
746
    )
725 747
    exp = start + idtoken_duration(client)
726 748
    id_token.update(
727 749
        {
728
-