Projet

Général

Profil

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

Frédéric Péters, 19 mai 2015 20:39

Télécharger (9,43 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.
 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 json
3
import logging
4
import requests
5
import urllib
6
import urlparse
7
import hashlib
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_id', 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__, self.__class__.__name__))
38
        self.seen_ids = set()
39

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

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

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

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

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

  
110
    requires_system_checks = False
111

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

  
118
    def handle_tenant(self, tenant, **options):
119
        # extract informations on deployed w.c.s. instances from hobo.json
120
        hobo_json_path = os.path.join(tenant.get_directory(), 'hobo.json')
121
        if not os.path.exists(hobo_json_path):
122
            print 'skipping %s, no hobo.json found' % tenant
123
            return
124
        hobo_environment = json.load(open(hobo_json_path))
125
        # compute our credentials from our hobo configuration
126
        me = [x for x in hobo_environment['services'] if x.get('this')]
127
        if not me:
128
            print 'skipping %s, self services is not marked' % tenant
129
            return
130
        me = me[0]
131
        orig = urlparse.urlsplit(me['base_url']).netloc.split(':')[0]
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='@', is_superuser=True)[0].email
136
        for service in hobo_environment['services']:
137
            if not service.get('saml-sp-metadata-url'):
138
                continue
139
            if not service.get('service-id') == 'wcs':
140
                continue
141
            liberty_provider = LibertyProvider.objects.get(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
-