Projet

Général

Profil

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

Benjamin Dauvergne, 26 mai 2015 11:30

Télécharger (9,32 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        | 149 +++++++++++++++++++++
 hobo/signature.py                                  |  37 +++++
 3 files changed, 199 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
        for role in response.json()['data']:
101
            yield Role(name=role['text'], external_id=str(role['slug']),
102
                       slug=str(role['slug']))
103

  
104

  
105
class Command(BaseCommand):
106
    option_list = BaseCommand.option_list + (
107
        make_option('--delete', action='store_true', dest='delete'),
108
    )
109
    help = "Import W.C.S. roles"
110

  
111
    requires_system_checks = False
112

  
113
    def handle(self, *args, **options):
114
        # traverse list of tenants
115
        for tenant in TenantMiddleware.get_tenants():
116
            with tenant_context(tenant):
117
                self.handle_tenant(tenant, **options)
118

  
119
    def handle_tenant(self, tenant, **options):
120
        # extract informations on deployed w.c.s. instances from hobo.json
121
        hobo_json_path = os.path.join(tenant.get_directory(), 'hobo.json')
122
        if not os.path.exists(hobo_json_path):
123
            print 'skipping %s, no hobo.json found' % tenant
124
            return
125
        hobo_environment = json.load(open(hobo_json_path))
126
        # compute our credentials from our hobo configuration
127
        me = [x for x in hobo_environment['services'] if x.get('this')]
128
        if not me:
129
            print 'skipping %s, self services is not marked' % tenant
130
            return
131
        orig = urlparse.urlsplit(me['base_url']).netloc
132
        key = hashlib.sha1(orig+me['secret_key']).hexdigest()
133
        # FIXME: get mail of the oldest superuser, could we do better ?
134
        User = get_user_model()
135
        email = User.objects.order_by('id').filter(email__contains='@',
136
                                                   is_superuser=True)[0].email
137
        for service in hobo_environment['services']:
138
            if not service.get('saml-sp-metadata-url'):
139
                continue
140
            liberty_provider = LibertyProvider.objects.get(
141
                entity_id=service['saml-sp-metadata-url'])
142
            importer = WcsRoleImporter(
143
                liberty_provider=liberty_provider,
144
                key=key,
145
                orig=orig,
146
                email=email,
147
                delete=options.get('delete', False),
148
            )
149
            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
-