Projet

Général

Profil

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

Benjamin Dauvergne, 09 mai 2015 10:19

Télécharger (9,09 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.

   ./authentic2-ctl import-wcs-roles --slug <slug-of-the-wcs-saml-service> --key key --orig orig --email <email> [--delete]

Use the --delete option if you want roles which have disappeared to be removed.
 README                                             |  13 ++
 .../management/commands/import-wcs-roles.py        | 142 +++++++++++++++++++++
 hobo/signature.py                                  |  37 ++++++
 3 files changed, 192 insertions(+)
 create mode 100644 hobo/agent/authentic2/management/commands/import-wcs-roles.py
 create mode 100644 hobo/signature.py
README
134 134
(created from settings / export) stored in /var/lib/wcs/skeletons (the exact
135 135
directory may vary according to the wcs configuration).
136 136

  
137
 - authentic2
138

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

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

  
7
from optparse import make_option
8

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

  
13
from authentic2.saml.models import LibertyProvider
14
from authentic2.a2_rbac.models import Role, RoleAttribute
15

  
16

  
17
from hobo import signature
18
from hobo.multitenant.middleware import TenantMiddleware
19

  
20
from tenant_schemas.utils import tenant_context
21

  
22
from .. import role_extractors
23

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

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

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

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

  
89
    def get_roles(self):
90
        '''Get w.c.s. from its roles web-service by sending a signed GET request'''
91
        url = self.wcs_url + 'roles?%s' % urllib.urlencode({'orig': self.orig, 'email': self.email})
92
        signed_url = signature.sign_url(url, self.key)
93
        response = requests.get(signed_url)
94
        for role in response.json()['data']:
95
            yield Role(
96
                    name=role['text'],
97
                    external_id=str(role['slug']),
98
                    slug=str(role['slug']))
99

  
100
class Command(BaseCommand):
101
    option_list = BaseCommand.option_list + (
102
        make_option('--delete', action='store_true', dest='delete'),
103
    )
104
    help = "Import W.C.S. roles"
105

  
106
    requires_system_checks = False
107

  
108
    def handle(self, *args, **options):
109
        # traverse list of tenants
110
        for tenant in TenantMiddleware.get_tenants():
111
            with tenant_context(tenant):
112
                self.handle_tenant(tenant, **options)
113

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