Projet

Général

Profil

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

Benjamin Dauvergne, 26 mai 2015 11:04

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

  
105

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

  
112
    requires_system_checks = False
113

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

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