Projet

Général

Profil

0001-agent-authentic2-add-new-command-import-wcs-roles.patch

Frédéric Péters, 26 mai 2015 13:04

Télécharger (9,63 ko)

Voir les différences:

Subject: [PATCH] agent/authentic2: add new command import-wcs-roles

The command make a signed get to the "roles" web-service of w.c.s. and try to
created services roles for all found roles. It traverses all tenants and
generate credentials from hobo.json keys (base_url and secret_key) and existing
superusers:

   ./authentic2-ctl import-wcs-roles [--delete]

Use the --delete option if you want roles which have disappeared to be removed.

fixes #7176
 README                                             |  13 ++
 .../management/commands/import-wcs-roles.py        | 156 +++++++++++++++++++++
 hobo/signature.py                                  |  37 +++++
 3 files changed, 206 insertions(+)
 create mode 100644 hobo/agent/authentic2/management/commands/import-wcs-roles.py
 create mode 100644 hobo/signature.py
README
147 147
(created from settings / export) stored in /var/lib/wcs/skeletons (the exact
148 148
directory may vary according to the wcs configuration).
149 149

  
150
 - authentic2
151

  
152
authentic2 instances will be deployed using "/usr/bin/authentic2-ctl" by
153
default, this command can be adapted in the AUTHENTIC_MANAGE_COMMAND setting.
154
It should be run with the same rights as the authentic2 process (redefine the
155
command to use sudo if necessary).
156

  
157
The agent also provide a commands to import roles from w.c.s named
158
import-wcs-roles. It computes the web-service credentials from the hobo.json
159
and use the email of the oldest superuser. Cron job can be created for calling
160
this command when regular synchronization of roles with your w.c.s.  instances
161
is needed. The sole option named "--delete" indicate if you want to delete
162
stale roles, default is to not delete them.
hobo/agent/authentic2/management/commands/import-wcs-roles.py
1
import os
2
import logging
3
import requests
4
import urllib
5
import urlparse
6
import hashlib
7
import json
8

  
9
from optparse import make_option
10

  
11
from django.utils.text import slugify
12
from django.core.management.base import BaseCommand
13
from django.contrib.auth import get_user_model
14

  
15
from authentic2.saml.models import LibertyProvider
16
from authentic2.a2_rbac.models import Role, RoleAttribute
17

  
18

  
19
from hobo import signature
20
from hobo.multitenant.middleware import TenantMiddleware
21

  
22
from tenant_schemas.utils import tenant_context
23

  
24

  
25
class WcsRoleImporter(object):
26
    def __init__(self, liberty_provider, key, orig, email,
27
                 attribute_name='role-slug', delete=False):
28
        self.service = liberty_provider
29
        self.slug = liberty_provider.slug
30
        self.key = key
31
        self.orig = orig
32
        self.email = email
33
        self.attribute_name = attribute_name
34
        self.delete = delete
35
        assert 'saml/metadata' in self.service.entity_id
36
        self.wcs_url = self.service.entity_id.split('saml/metadata')[0]
37
        self.logger = logging.getLogger('%s.%s' % (__name__,
38
                                                   self.__class__.__name__))
39
        self.seen_ids = set()
40

  
41
    def import_roles(self):
42
        for role_tpl in self.get_roles():
43
            self.seen_ids.add(role_tpl.external_id)
44
            self.create_role(role_tpl)
45
        if self.delete:
46
            self.delete_dead_roles()
47

  
48
    def create_role(self, role_tpl):
49
        defaults = {
50
            'name': role_tpl.name,
51
            # w.c.s. will always provide a slug but for other services we do
52
            # not know
53
            'slug': role_tpl.slug or slugify(role_tpl.name),
54
        }
55
        # search role by external id, create if not found
56
        role, created = Role.objects.get_or_create(
57
            service=self.service,
58
            external_id=role_tpl.external_id,
59
            defaults=defaults)
60
        RoleAttribute.objects.filter(role=role).delete()
61
        role_attribute, ra_created = RoleAttribute.objects.get_or_create(
62
            role=role,
63
            name=self.attribute_name,
64
            kind='string',
65
            defaults={
66
                'value': role_tpl.external_id
67
            })
68
        if created:
69
            self.logger.info('imported new role %r(%r) from service %s',
70
                             role.external_id, role.name, self.slug)
71
        # update role attribute value if it has changed
72
        if not ra_created:
73
            if role_attribute.value != role_tpl.external_id:
74
                role_attribute.value = role_tpl.external_id
75
                role_attribute.save()
76
        # update role name if has changed
77
        if not created:
78
            # Update name and slug if they have changed
79
            if role.name != role_tpl.name:
80
                role.name = role_tpl.name
81
                role.save()
82

  
83
    def delete_dead_roles(self):
84
        '''Deletes service roles whose id is not in self.seen_ids'''
85
        qs = Role.objects.filter(service=self.service) \
86
            .exclude(external_id__in=list(self.seen_ids))
87
        for role in qs:
88
            self.logger.info('deleted dead role %r(%r) from service %s',
89
                             role.external_id, role.slug, self.slug)
90
        qs.delete()
91

  
92
    def get_roles(self):
93
        '''Get w.c.s. from its roles web-service by sending a signed GET
94
           request.
95
        '''
96
        url = self.wcs_url + 'api/roles?%s' % urllib.urlencode(
97
            {'format': 'json', 'orig': self.orig, 'email': self.email})
98
        signed_url = signature.sign_url(url, self.key)
99
        response = requests.get(signed_url)
100
        if response.status_code == 200:
101
            for role in response.json()['data']:
102
                yield Role(name=role['text'], external_id=str(role['slug']),
103
                           slug=str(role['slug']))
104
        else:
105
            self.logger.warn('failed to get roles for %s (response: %s)',
106
                    self.wcs_url, response.status_code)
107

  
108

  
109
class Command(BaseCommand):
110
    option_list = BaseCommand.option_list + (
111
        make_option('--delete', action='store_true', dest='delete'),
112
    )
113
    help = "Import W.C.S. roles"
114

  
115
    requires_system_checks = False
116

  
117
    def handle(self, *args, **options):
118
        # traverse list of tenants
119
        for tenant in TenantMiddleware.get_tenants():
120
            with tenant_context(tenant):
121
                self.handle_tenant(tenant, **options)
122

  
123
    def handle_tenant(self, tenant, **options):
124
        # extract informations on deployed w.c.s. instances from hobo.json
125
        hobo_json_path = os.path.join(tenant.get_directory(), 'hobo.json')
126
        if not os.path.exists(hobo_json_path):
127
            print 'skipping %s, no hobo.json found' % tenant
128
            return
129
        hobo_environment = json.load(open(hobo_json_path))
130
        # compute our credentials from our hobo configuration
131
        me = [x for x in hobo_environment['services'] if x.get('this')]
132
        if not me:
133
            print 'skipping %s, self services is not marked' % tenant
134
            return
135
        me = me[0]
136
        orig = urlparse.urlsplit(me['base_url']).netloc.split(':')[0]
137
        key = hashlib.sha1(orig+me['secret_key']).hexdigest()
138
        # FIXME: get mail of the oldest superuser, could we do better ?
139
        User = get_user_model()
140
        email = User.objects.order_by('id').filter(email__contains='@',
141
                                                   is_superuser=True)[0].email
142
        for service in hobo_environment['services']:
143
            if not service.get('service-id') == 'wcs':
144
                continue
145
            if not service.get('saml-sp-metadata-url'):
146
                continue
147
            liberty_provider = LibertyProvider.objects.get(
148
                entity_id=service['saml-sp-metadata-url'])
149
            importer = WcsRoleImporter(
150
                liberty_provider=liberty_provider,
151
                key=key,
152
                orig=orig,
153
                email=email,
154
                delete=options.get('delete', False),
155
            )
156
            importer.import_roles()
hobo/signature.py
1
import base64
2
import hmac
3
import hashlib
4
import datetime
5
import urllib
6
import urllib2
7
import urlparse
8
import random
9

  
10
def sign_url(url, key, algo='sha256', timestamp=None, nonce=None):
11
    parsed = urlparse.urlparse(url)
12
    new_query = sign_query(parsed.query, key, algo, timestamp, nonce)
13
    return urlparse.urlunparse(parsed[:4] + (new_query,) + parsed[5:])
14

  
15
def sign_query(query, key, algo='sha256', timestamp=None, nonce=None):
16
    if timestamp is None:
17
        timestamp = datetime.datetime.utcnow()
18
    timestamp = timestamp.strftime('%Y-%m-%dT%H:%M:%SZ')
19
    if nonce is None:
20
        nonce = hex(random.getrandbits(128))[2:-1]
21
    new_query = query
22
    if new_query:
23
        new_query += '&'
24
    new_query += urllib.urlencode((
25
        ('algo', algo),
26
        ('timestamp', timestamp),
27
        ('nonce', nonce)))
28
    signature = base64.b64encode(sign_string(new_query, key, algo=algo))
29
    new_query += '&signature=' + urllib.quote(signature)
30
    return new_query
31

  
32
def sign_string(s, key, algo='sha256', timedelta=30):
33
    digestmod = getattr(hashlib, algo)
34
    hash = hmac.HMAC(key, digestmod=digestmod, msg=s)
35
    return hash.digest()
36

  
37

  
0
-