#137 Add consent system and user portal
Merged 7 years ago by merlinthp. Opened 7 years ago by merlinthp.
merlinthp/ipsilon userportal  into  master

file modified
+11 -1
@@ -48,10 +48,11 @@ 

  

  class ProviderBase(ConfigHelper, PluginObject):

  

-     def __init__(self, name, path, *pargs):

+     def __init__(self, name, displayname, path, *pargs):

          ConfigHelper.__init__(self)

          PluginObject.__init__(self, *pargs)

          self.name = name

+         self.displayname = displayname

          self._root = None

          self.path = path

          self.tree = None
@@ -75,6 +76,15 @@ 

      def get_providers(self):

          return []

  

+     def get_display_name(self):

+         return self.displayname

+ 

+     def get_client_display_name(self, clientid):

+         raise NotImplementedError

+ 

+     def consent_to_display(self, consentdata):

+         raise NotImplementedError

+ 

  

  class ProviderPageBase(Page):

  

@@ -530,6 +530,24 @@ 

          h = hashlib.sha256(msg.encode()).digest()

          return base64.urlsafe_b64encode(h[:16]).rstrip(b'=').decode()

  

+     def _valid_claims(self, claims, userattrs):

+         d = []

+         for claimtype in claims:

+             for claim in claims[claimtype]:

+                 if claim in userattrs:

+                     d.append(claim)

+         return sorted(d)

+ 

+     def _valid_scopes(self, scopes):

+         d = []

+         for dummy_n, e in self.cfg.extensions.available().items():

+             if e.enabled:

+                 extscopes = e.get_scopes()

+                 for scope in scopes:

+                     if scope in extscopes:

+                         d.append(scope)

+         return sorted(d)

+ 

      def _perform_continue(self, *args, **kwargs):

          us = UserSession()

          user = us.get_user()
@@ -571,6 +589,27 @@ 

                                           user,

                                           userattrs)

  

+         consentdata = user.get_consent('openidc', request_data['client_id'])

+         if consentdata is not None:

+             # Consent has already been granted

+             self.debug('Consent already granted')

+ 

+             consclaimset = set(consentdata['claims'])

+             claimset = set(self._valid_claims(request_data['claims'],

+                                               userattrs))

+             consscopeset = set(consentdata['scopes'])

+             scopeset = set(self._valid_scopes(request_data['scope']))

+ 

+             if claimset.issubset(consclaimset) and \

+                     scopeset.issubset(consscopeset):

+                 return self._respond_success(request_data,

+                                              client,

+                                              user,

+                                              userattrs)

+             else:

+                 self.debug('Client is asking for new claims or scopes, user '

+                            'must give consent again')

+ 

          if 'none' in request_data['prompt']:

              # We were asked to not show any UI

              return self._respond_error(request_data,
@@ -582,6 +621,17 @@ 

              # The user has been shown the form, let's process his choice

              if 'decided_allow' in kwargs:

                  # User allowed the request

+ 

+                 # Record the consent for the future, including the list of

+                 # claims that the user was informed of at the time.

+                 consentdata = {

+                     'claims': self._valid_claims(request_data['claims'],

+                                                  userattrs),

+                     'scopes': self._valid_scopes(request_data['scope'])

+                 }

+                 user.grant_consent('openidc', request_data['client_id'],

+                                    consentdata)

+ 

                  return self._respond_success(request_data,

                                               client,

                                               user,
@@ -599,7 +649,6 @@ 

                      'openidc_request': json.dumps(request_data)}

              self.trans.store(data)

  

-             userattrs = self._source_attributes(us)

              claim_requests = {}

              for claimtype in request_data['claims']:

                  for claim in request_data['claims'][claimtype]:

file modified
+22 -1
@@ -21,7 +21,8 @@ 

  class IdpProvider(ProviderBase):

  

      def __init__(self, *pargs):

-         super(IdpProvider, self).__init__('openidc', 'openidc', *pargs)

+         super(IdpProvider, self).__init__('openidc', 'OpenID Connect',

+                                           'openidc', *pargs)

          self.mapping = InfoMapping()

          self.keyset = None

          self.admin = None
@@ -202,6 +203,26 @@ 

              'http://openid.net/specs/connect/1.0/issuer'

          )

  

+     def get_client_display_name(self, clientid):

+         return self.datastore.getClient(clientid)['client_name']

+ 

+     def consent_to_display(self, consentdata):

+         d = []

+ 

+         if len(consentdata['scopes']) > 0:

+             scopes = []

+             for dummy_n, e in self.extensions.available().items():

+                 data = e.get_display_data(consentdata['scopes'])

+                 if len(data) > 0:

+                     scopes.append(e.get_display_name())

+             d.append('Scopes: %s' % ', '.join(sorted(scopes)))

+ 

+         if len(consentdata['claims']) > 0:

+             d.append('Claims: %s' % ', '.join([self.mapping.display_name(x) for

+                                                x in consentdata['claims']]))

+ 

+         return d

+ 

  

  class Installer(ProviderInstaller):

  

file modified
+7 -1
@@ -16,7 +16,7 @@ 

  class IdpProvider(ProviderBase):

  

      def __init__(self, *pargs):

-         super(IdpProvider, self).__init__('openid', 'openid', *pargs)

+         super(IdpProvider, self).__init__('openid', 'OpenID', 'openid', *pargs)

          self.mapping = InfoMapping()

          self.page = None

          self.datastore = None
@@ -135,6 +135,12 @@ 

          self.init_idp()

          self.extensions.enable(self._config['enabled extensions'].get_value())

  

+     def get_client_display_name(self, clientid):

+         return clientid

+ 

+     def consent_to_display(self, consentdata):

+         return []

+ 

  

  class Installer(ProviderInstaller):

  

@@ -17,7 +17,8 @@ 

  class IdpProvider(ProviderBase):

  

      def __init__(self, *pargs):

-         super(IdpProvider, self).__init__('persona', 'persona', *pargs)

+         super(IdpProvider, self).__init__('persona', 'Persona', 'persona',

+                                           *pargs)

          self.mapping = InfoMapping()

          self.page = None

          self.basepath = None
@@ -72,6 +73,12 @@ 

          super(IdpProvider, self).on_enable()

          self.init_idp()

  

+     def get_client_display_name(self, clientid):

+         return clientid

+ 

+     def consent_to_display(self, consentdata):

+         return []

+ 

  

  class Installer(ProviderInstaller):

  

@@ -217,7 +217,7 @@ 

  class IdpProvider(ProviderBase):

  

      def __init__(self, *pargs):

-         super(IdpProvider, self).__init__('saml2', 'saml2', *pargs)

+         super(IdpProvider, self).__init__('saml2', 'SAML 2.0', 'saml2', *pargs)

          self.admin = None

          self.rest = None

          self.page = None
@@ -456,6 +456,12 @@ 

          self.debug('Sending initial logout request to %s' % logout.msgUrl)

          raise cherrypy.HTTPRedirect(logout.msgUrl)

  

+     def get_client_display_name(self, clientid):

+         return clientid

+ 

+     def consent_to_display(self, consentdata):

+         return []

+ 

  

  class IdpMetadataGenerator(object):

  

file modified
+2
@@ -14,6 +14,7 @@ 

  from ipsilon.admin.authz import AuthzPlugins

  from ipsilon.rest.common import Rest

  from ipsilon.rest.providers import RestProviderPlugins

+ from ipsilon.user.common import UserPortal

  from ipsilon.authz.common import Authz

  import cherrypy

  
@@ -52,6 +53,7 @@ 

          self.admin = Admin(self._site, 'admin')

          self.rest = Rest(self._site, 'rest')

          self.stack = LoginStack(self._site, self.admin)

+         self.portal = UserPortal(self._site, 'portal')

          LoginPlugins(self._site, self.stack)

          InfoPlugins(self._site, self.stack)

          AuthzPlugins(self._site, self.stack)

empty or binary file added
@@ -0,0 +1,62 @@ 

+ # Copyright (C) 2016 Ipsilon project Contributors, for license see COPYING

+ 

+ from ipsilon.util.page import Page

+ from ipsilon.util.user import UserSession

+ import cherrypy

+ 

+ 

+ class UserPortalPage(Page):

+     def __init__(self, *args, **kwargs):

+         super(UserPortalPage, self).__init__(*args, **kwargs)

+         self.auth_protect = True

+ 

+ 

+ class UserPortalConsent(UserPortalPage):

+     def __init__(self, site, parent, mount):

+         super(UserPortalConsent, self).__init__(site)

+         self._master = parent

+         self.title = 'User portal consent'

+         self.url = '%s/%s' % (parent.url, 'consent')

+         self.menu = [self]

+ 

+     def revoke(self, provider, clientid):

+         us = UserSession()

+         user = us.get_user()

+         user.revoke_consent(provider, clientid)

+         raise cherrypy.HTTPRedirect(self._master.url)

+     revoke.public_function = True

+ 

+ 

+ class UserPortal(UserPortalPage):

+     def __init__(self, site, mount):

+         super(UserPortal, self).__init__(site)

+         self.title = 'User portal'

+         self.url = '%s/%s' % (self.basepath, mount)

+         self.menu = [self]

+         self.consent = UserPortalConsent(site, self, 'consent')

+ 

+     def root(self, *args, **kwargs):

+         us = UserSession()

+         user = us.get_user()

+         consents = user.list_consents()

+ 

+         for consent in consents:

+             provname = consent['provider']

+             provider = self._site['provider_config'].available.get(provname,

+                                                                    None)

+ 

+             if provider is not None:

+                 consent['providerdn'] = provider.get_display_name()

+                 consent['clientdn'] = provider.\

+                     get_client_display_name(consent['client'])

+                 attrs = provider.consent_to_display(consent['attrs'])

+             else:

+                 self.debug('Consent relates to unknown provider %s' % provname)

+                 attrs = []

+             consent['attrs'] = attrs

+ 

+         return self._template('user/index.html',

+                               title='',

+                               baseurl=self.url,

+                               menu=self.menu,

+                               consents=consents)

file modified
+74 -7
@@ -544,6 +544,10 @@ 

                  else:

                      q.insert((name, opt, options[opt]))

  

+             for opt in curvals:

+                 if opt not in options:

+                     q.delete({'name': name, 'option': opt})

+ 

              q.commit()

          except Exception, e:  # pylint: disable=broad-except

              if q:
@@ -729,11 +733,78 @@ 

      def load_plugin_data(self, plugin, user):

          return self.load_options(plugin+"_data", user)

  

-     def _initialize_schema(self):

-         q = self._query(self._db, 'users', OPTIONS_TABLE, trans=False)

+     def _cons_key(self, provider, clientid):

+         return '%s-%s' % (provider, clientid)

+ 

+     def _split_cons_key(self, key):

+         return key.split('-', 1)

+ 

+     def store_consent(self, user, provider, clientid, parameters):

+         q = None

+         try:

+             key = self._cons_key(provider, clientid)

+             q = self._query(self._db, 'user_consent', OPTIONS_TABLE)

+             rows = q.select({'name': user, 'option': key}, ['value'])

+             if len(list(rows)) > 0:

+                 q.update({'value': parameters}, {'name': user, 'option': key})

+             else:

+                 q.insert((user, key, parameters))

+             q.commit()

+         except Exception, e:  # pylint: disable=broad-except

+             if q:

+                 q.rollback()

+             self.error('Failed to store consent: [%s]' % e)

+             raise

+ 

+     def delete_consent(self, user, provider, clientid):

+         q = None

+         try:

+             q = self._query(self._db, 'user_consent', OPTIONS_TABLE)

+             q.delete({'name': user,

+                       'option': self._cons_key(provider, clientid)})

+             q.commit()

+         except Exception, e:  # pylint: disable=broad-except

+             if q:

+                 q.rollback()

+             self.error('Failed to delete consent: [%s]' % e)

+             raise

+ 

+     def get_consent(self, user, provider, clientid):

+         try:

+             q = self._query(self._db, 'user_consent', OPTIONS_TABLE)

+             rows = q.select({'name': user,

+                              'option': self._cons_key(provider, clientid)},

+                             ['value'])

+             data = list(rows)

+             if len(data) > 0:

+                 return data[0][0]

+             else:

+                 return None

+         except Exception, e:  # pylint: disable=broad-except

+             self.error('Failed to get consent: [%s]' % e)

+             return None

+ 

+     def get_all_consents(self, user):

+         d = []

+         try:

+             q = self._query(self._db, 'user_consent', OPTIONS_TABLE)

+             rows = q.select({'name': user}, ['option', 'value'])

+             for r in rows:

+                 prov, clientid = self._split_cons_key(r[0])

+                 d.append((prov, clientid, r[1]))

+         except Exception, e:  # pylint: disable=broad-except

+             self.error('Failed to get consents: [%s]' % e)

+         return d

+ 

+     def _initialize_table(self, tablename):

+         q = self._query(self._db, tablename, OPTIONS_TABLE, trans=False)

          q.create()

          q._con.close()  # pylint: disable=protected-access

  

+     def _initialize_schema(self):

+         self._initialize_table('users')

+         self._initialize_table('user_consent')

+ 

      def _upgrade_schema(self, old_version):

          if old_version == 1:

              # In schema version 2, we added indexes and primary keys
@@ -751,11 +822,7 @@ 

  

      def create_plugin_data_table(self, plugin_name):

          if not self.is_readonly:

-             table = plugin_name+'_data'

-             q = self._query(self._db, table, OPTIONS_TABLE,

-                             trans=False)

-             q.create()

-             q._con.close()  # pylint: disable=protected-access

+             self._initialize_table(plugin_name + '_data')

  

  

  class TranStore(Store):

file modified
+31
@@ -4,6 +4,7 @@ 

  from ipsilon.util.log import Log

  import cherrypy

  import logging

+ import json

  

  

  class Site(object):
@@ -90,6 +91,36 @@ 

          store = UserStore()

          return store.load_plugin_data(plugin, self.name)

  

+     def grant_consent(self, provider, clientid, parameters):

+         store = UserStore()

+         store.store_consent(self.name, provider, clientid,

+                             json.dumps(parameters))

+ 

+     def revoke_consent(self, provider, clientid):

+         store = UserStore()

+         store.delete_consent(self.name, provider, clientid)

+ 

+     def get_consent(self, provider, clientid):

+         store = UserStore()

+         data = store.get_consent(self.name, provider, clientid)

+         if data is not None:

+             return json.loads(data)

+         return None

+ 

+     def list_consents(self, provider=None):

+         store = UserStore()

+         d = []

+         for prov, clientid, parameters in store.get_all_consents(self.name):

+             if provider is not None:

+                 if prov != provider:

+                     continue

+             d.append({

+                 'provider': prov,

+                 'client': clientid,

+                 'attrs': json.loads(parameters)

+             })

+         return d

+ 

  

  class UserSession(Log):

      def __init__(self):

file modified
+1
@@ -48,6 +48,7 @@ 

  USERS_TEMPLATE='''

  CREATE TABLE users(name TEXT, option TEXT, value TEXT);

  INSERT INTO users VALUES('admin', 'is_admin', '1');

+ CREATE TABLE user_consent(name TEXT, option TEXT, value TEXT);

  '''

  

  def config(workdir):

@@ -37,7 +37,7 @@ 

        {%- endif %}

        <div class="row ipsilon-row {{ highlight }}">

          <div class="col-md-3 col-sm-3 col-xs-6">

-           <p><strong>{{ p }}</strong></p>

+           <p><strong>{{ available[p].get_display_name() }}</strong></p>

          </div>

          <div class="col-md-9 col-sm-9 col-xs-6">

            <div class="row">

@@ -14,7 +14,7 @@ 

              <a href="{{ baseurl }}/client/{{ cid }}">{{ cid }}</a>

          </div>

          <div class="col-md-3 col-sm-3 col-xs-6">

-              {{ cid }}

+              {{ clients[cid]['client_name'] }}

              <a class="btn btn-default" href="{{ baseurl }}/client/{{ cid }}/delete"

                  onclick="return confirm('Do you really want to remove this client?');">Delete</a>

          </div>

file modified
+4
@@ -31,6 +31,10 @@ 

                </a>

              <ul class="dropdown-menu">

                <li>

+                 <a href="{{ basepath }}/portal" id="userportal">Self service</a>

+               </li>

+               <li class="divider"></li>

+               <li>

                  <a href="{{ basepath }}/logout" id="logout">Log Out</a>

                </li>

              </ul>

@@ -51,6 +51,10 @@ 

              </a>

              <ul class="dropdown-menu">

                <li>

+                 <a href="{{ basepath }}/portal" id="userportal">Self service</a>

+               </li>

+               <li class="divider"></li>

+               <li>

                  <a href="{{ basepath }}/logout" id="logout">Log Out</a>

                </li>

              </ul>

@@ -0,0 +1,55 @@ 

+ <!DOCTYPE html>

+ <html>

+   <head>

+     <title>{{ title }}</title>

+     <meta charset="utf-8" /> 

+     <meta name="viewport" content="width=device-width, initial-scale=1.0">

+     <link href="{{ basepath }}/ui/css/patternfly.css" rel="stylesheet" media="screen, print">

+     <link href="{{ basepath }}/ui/css/styles.css" rel="stylesheet" media="screen, print">

+   </head>

+   <body class="cards-pf">

+     <nav class="navbar navbar-default navbar-pf navbar-pf-lg" role="navigation">

+       <div class="navbar-header">

+         <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse-1">

+           <span class="sr-only">Toggle navigation</span>

+           <span class="icon-bar"></span>

+           <span class="icon-bar"></span>

+           <span class="icon-bar"></span>

+         </button>

+         <a class="navbar-brand" href="{{ basepath }}/">

+           <img type="image/svg+xml" src="{{ basepath }}/ui/img/brand.png" alt="Ipsilon" width="auto" height="10px" />

+         </a>

+       </div>

+       <div class="collapse navbar-collapse navbar-collapse-1">

+         {% if user.name %}

+         <ul class="nav navbar-nav navbar-utility">

+           <li class="dropdown">

+               <a href="#" class="dropdown-toggle" data-toggle="dropdown">

+                 <span class="pficon pficon-user"></span>

+                 {{ user.fullname }}<b class="caret"></b>

+               </a>

+             <ul class="dropdown-menu">

+               <li>

+                 <a href="{{ basepath }}/portal" id="userportal">Self service</a>

+               </li>

+               <li class="divider"></li>

+               <li>

+                 <a href="{{ basepath }}/logout" id="logout">Log Out</a>

+               </li>

+             </ul>

+           </li>

+         </ul>

+         {% endif %}

+       </div>

+     </nav>

+ 

+         {% block main %}

+         {% endblock %}

+ 

+     <!-- JS -->

+     <script src="{{ basepath }}/ui/js/jquery.js"></script>

+     <script src="{{ basepath }}/ui/js/bootstrap.js"></script>

+     <script src="{{ basepath }}/ui/js/patternfly.js"></script>

+     <script src="{{ basepath }}/ui/js/divfilter.js"></script>

+   </body>

+ </html>

@@ -0,0 +1,48 @@ 

+ {% extends "master-portal.html" %}

+ {% block main %}

+ <div class="container">

+   <div class="row">

+     <div class="col-sm-12">

+       <h2>Granted Consent</h2>

+       <div class="row">

+         <div class="col-xs-2">

+           <p><b>Provider</b></p>

+         </div>

+         <div class="col-xs-4">

+           <p><b>Client</b></p>

+         </div>

+         <div class="col-xs-4">

+           <p><b>Consent Data</b></p>

+         </div>

+         <div class="col-xs-2">

+         </div>

+       </div>

+ {%- for item in consents %}

+       <div class="row ipsilon-row">

+         <div class="col-xs-2">

+           <p><strong>{{ item.providerdn }}</strong></p>

+         </div>

+         <div class="col-xs-4">

+           <p>{{ item.clientdn }}</p>

+         </div>

+         <div class="col-xs-4">

+ {%- for attr in item.attrs %}

+           <p>{{ attr }}</p>

+ {%- endfor %}

+         </div>

+         <div class="col-xs-2">

+             <a id="revoke-{{ item.client }}" class="btn btn-default" href="{{ baseurl }}/consent/revoke/{{ item.provider }}/{{ item.client }}" onclick="return confirm('Do you really want to revoke this consent?');">Revoke</a>

+         </div>

+       </div>

+ {%- endfor %}

+     </div>

+   </div>

+   <!--

+   <div class="row">

+     <div class="col-sm-12">

+       <h2>Active Sessions</h2>

+     </div>

+   </div>

+   -->

+ </div>

+ {% endblock %}

file modified
+41 -1
@@ -268,7 +268,8 @@ 

  

          return ['post', url, {'data': params}]

  

-     def fetch_page(self, idp, target_url, follow_redirect=True, krb=False):

+     def fetch_page(self, idp, target_url, follow_redirect=True, krb=False,

+                    require_consent=None):

          """

          Fetch a page and parse the response code to determine what to do

          next.
@@ -276,10 +277,15 @@ 

          The login process consists of redirections (302/303) and

          potentially an unauthorized (401). For the case of unauthorized

          try the page returned in case of fallback authentication.

+ 

+         require_consent indicates whether consent should or should not be asked

+         or if that's not in this test. None means not tested, False means must

+         not be asked, True means must be asked.

          """

          url = target_url

          action = 'get'

          args = {}

+         seen_consent = False

  

          while True:

              r = self.access(action, url, krb=krb, **args)
@@ -317,12 +323,14 @@ 

  

                  try:

                      (action, url, args) = self.handle_openid_consent_form(page)

+                     seen_consent = True

                      continue

                  except WrongPage:

                      pass

  

                  try:

                      (action, url, args) = self.handle_openid_form(page)

+                     seen_consent = True

                      continue

                  except WrongPage:

                      pass
@@ -334,6 +342,13 @@ 

                      pass

  

                  # Either we got what we wanted, or we have to stop anyway

+                 if (not seen_consent) and require_consent:

+                     raise ValueError('IDP did not present consent page, but '

+                                      'consent is required.')

+                 elif seen_consent and (require_consent is False):

+                     raise ValueError('IDP presented consent page, but '

+                                      'consent is disallowed.')

+ 

                  return page

              else:

                  raise ValueError("Unhandled status (%d) on url %s" % (
@@ -574,6 +589,31 @@ 

          if client_id in r.text:

              raise ValueError('Client was not gone after deletion')

  

+     def revoke_oidc_consent(self, idp):

+         """

+         Revoke user's consent for all OpenIDC clients.

+         """

+         idpsrv = self.servers[idp]

+         idpuri = idpsrv['baseuri']

+ 

+         url = '%s%s/portal' % (idpuri, self.get_idp_uri(idp))

+         headers = {'referer': url}

+         r = idpsrv['session'].get(url, headers=headers)

+         if r.status_code != 200:

+             ValueError('Failed to load user portal [%s]' % repr(r))

+         page = PageTree(r)

+ 

+         revbtns = page.all_values('//a[starts-with(@id, "revoke-")]')

+ 

+         for btn in revbtns:

+             url = '%s%s' % (idpuri, btn.get('href'))

+             headers = {'referer': url}

+             headers['content-type'] = 'application/x-www-form-urlencoded'

+ 

+             r = idpsrv['session'].get(url, headers=headers)

+             if btn.get('id') in r.text:

+                 raise ValueError('Client was not gone after revoke')

+ 

      def fetch_rest_page(self, idpname, uri):

          """

          idpname - the name of the IDP to fetch the page from

file modified
+33 -1
@@ -245,7 +245,8 @@ 

  

      print "openidc: Access first SP Protected Area ...",

      try:

-         page = sess.fetch_page(idpname, 'https://127.0.0.11:45081/sp/')

+         page = sess.fetch_page(idpname, 'https://127.0.0.11:45081/sp/',

+                                require_consent=True)

          h = hashlib.sha256()

          h.update('127.0.0.11')

          h.update(user)
@@ -262,6 +263,37 @@ 

          sys.exit(1)

      print " SUCCESS"

  

+     print "openidc: Log back in to first SP Protected Area without consent" \

+         " ...",

+     try:

+         page = sess.fetch_page(idpname,

+                                'https://127.0.0.11:45081/sp/redirect_uri?log'

+                                'out=https%3A%2F%2F127.0.0.11%3A45081%2Fsp%2F',

+                                require_consent=False)

+     except ValueError, e:

+         print >> sys.stderr, " ERROR: %s" % repr(e)

+         sys.exit(1)

+     print " SUCCESS"

+ 

+     print "openidc: Revoking SP consent ...",

+     try:

+         page = sess.revoke_oidc_consent(idpname)

+     except ValueError, e:

+         print >> sys.stderr, "" % repr(e)

+         sys.exit(1)

+     print " SUCCESS"

+ 

+     print "openidc: Log back in to first SP Protected Area with consent ...",

+     try:

+         page = sess.fetch_page(idpname,

+                                'https://127.0.0.11:45081/sp/redirect_uri?log'

+                                'out=https%3A%2F%2F127.0.0.11%3A45081%2Fsp%2F',

+                                require_consent=True)

+     except ValueError, e:

+         print >> sys.stderr, " ERROR: %s" % repr(e)

+         sys.exit(1)

+     print " SUCCESS"

+ 

      print "openidc: Update first SP client name ...",

      try:

          sess.update_options(

no initial comment

I would prefer to have the consent stuff go directly into the database, so skip the self._consentdata dict.
This would allow you to do more direct queries against the backend that include the provider-key, so that we don't need to cache everything.

Also, you can just store json.dumps()'d data, and then you don't have to do manual splitting.

Note that for OIDC, we need both the claims, and the requested scopes, to be part of the consent framework.
This because the scopes are just as important (if not more so given that claims are based on requested scopes most of the time) than claims.

4 new commits added

  • Add a user self-service portal for managing consent
  • Plumb openidc into the consent system
  • Add consent system
  • When saving options, remove options from the db that no longer exist
7 years ago

Is there any reason you're not using save_options?

The old semantics of save_options were that we passed it a bundle of options. Any options that weren't in the DB were added to the DB, any options that were in a DB with a different value were changed in the DB, and any options that were in the DB but not in the bundle were left alone.

The previous version of this PR maintained a copy of the consent data in a dict, which was synchronised with the DB using save_options. The first patch in this PR (which is the same as the original version of the PR) changes the save_options semantics to remove options in the DB that aren't in the options passed to it. Without that, we could never delete any consent from the DB.

I'll drop the save_options semantics patch, switch this to using save_options, and delete_consent to using delete_options.

I think it would be useful to run this through the respective provider modules, so that they can make the consent information user readable.
I think it's depending on the module how best to summarize the contents of the consent. (for example, see the display_name for custom scopes in OpenIDC, and the default OIDC scopes should probably not be shown due to the fact that they're just the internal way to ask for claims).

Yep, I had a similar thought.

4 new commits added

  • Add a user self-service portal for managing consent
  • Plumb openidc into the consent system
  • Add consent system
  • When saving options, remove options from the db that no longer exist
7 years ago

4 new commits added

  • Add a user self-service portal for managing consent
  • Plumb openidc into the consent system
  • Add consent system
  • When saving options, remove options from the db that no longer exist
7 years ago

Still needs tests adding, and we need to decide what to do about the save_options stuff.

1 new commit added

  • Add test case for openidc consent
7 years ago

Left over debugging? :)

5 new commits added

  • Add test case for openidc consent
  • Add a user self-service portal for managing consent
  • Plumb openidc into the consent system
  • Add consent system
  • When saving options, remove options from the db that no longer exist
7 years ago

5 new commits added

  • Add test case for openidc consent
  • Add a user self-service portal for managing consent
  • Plumb openidc into the consent system
  • Add consent system
  • When saving options, remove options from the db that no longer exist
7 years ago

5 new commits added

  • Add test case for openidc consent
  • Add a user self-service portal for managing consent
  • Plumb openidc into the consent system
  • Add consent system
  • When saving options, remove options from the db that no longer exist
7 years ago

Remaining debugging output? :)

5 new commits added

  • Add test case for openidc consent
  • Add a user self-service portal for managing consent
  • Plumb openidc into the consent system
  • Add consent system
  • When saving options, remove options from the db that no longer exist
7 years ago

This comment is not exactly what the function does.

One last remark about a comment, and then it looks good to me.
Thanks!

5 new commits added

  • Add test case for openidc consent
  • Add a user self-service portal for managing consent
  • Plumb openidc into the consent system
  • Add consent system
  • When saving options, remove options from the db that no longer exist
7 years ago

Commit 05a6a4e fixes this pull-request

Pull-Request has been merged by merlin@merlinthp.org

7 years ago