Skip to content

Commit

Permalink
add features_permissions oauth
Browse files Browse the repository at this point in the history
  • Loading branch information
alexandredevely committed Nov 18, 2024
1 parent 261b9c3 commit 1867f24
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 60 deletions.
62 changes: 51 additions & 11 deletions controllers/auth_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import logging
import cherrypy
import chevron
import base64
import oc.od.tracking
from cryptography import x509
from cryptography.hazmat.backends import default_backend
Expand Down Expand Up @@ -178,7 +179,14 @@ def oauth(self, **params):

# overwrite auth params to prevent manager changes
# for security reasons
params['manager'] = 'external' # oauth MUST force an 'external' manager
params['manager'] = 'external' # oauth MUST force an 'external' manager

# base64 decode 'state' as 'features' if need
self.update_features_args( args=params )
# verify is features can be set
self.check_features_permissions( args=params )

# do login
response = services.auth.login(**params)

# can raise excetion
Expand Down Expand Up @@ -236,9 +244,13 @@ def auth(self):
# read user's client ipsource
ipsource = getclientipaddr()

# can raise excetion
# can raise exception
self.isban_ip(ipsource)

# verify if features is set and can be set
# can raise exception
self.check_features_permissions( args=args )

# if force_auth_prelogin
http_attribut_to_force_auth_prelogin = cherrypy.request.headers.get(services.prelogin.http_attribut_to_force_auth_prelogin)
self.logger.debug( f"dump http_attribut_to_force_auth_prelogin http.header[{services.prelogin.http_attribut_to_force_auth_prelogin}] = {http_attribut_to_force_auth_prelogin}" )
Expand Down Expand Up @@ -608,6 +620,43 @@ def checkloginresponseresult( self, response, msg='login' ):

self.logger.error( f"services auth.login error {message}" )
raise cherrypy.HTTPError(401, message )

def check_features_permissions( sefl, args:dict)->None:
# check if args contains a features dict
if isinstance( args.get('features'), dict ) :
# this request asks for custom features
# Check if features update are allowed
if 'submit' not in oc.od.settings.desktop['features_permissions']:
raise cherrypy.HTTPError(401, message="'submit' is not in desktop.features_permissions, update configuration file" )
else:
raise cherrypy.HTTPError(401, message="bad parameters features, features must be a dict" )

def update_features_args(self, args:dict)->None:
"""update_features_args
Args:
args (dict): params
Raises:
cherrypy.HTTPError: cherrypy.HTTPError(401) for bad parameters
cherrypy.HTTPError: cherrypy.HTTPError(401) for features disallow
"""
# features can be implemented as 'state' in querystring
# create 'features' entry in args dict
# as base64 decoded values
if isinstance( args, dict ):
# for ODExternalAuthManager
# features is named as 'state' and is base64 encoded
state = args.get('state')
if isinstance( state, str):
try:
decoded_state = base64.b64decode( state.encode('ascii') ).decode()
dict_state = json.loads( decoded_state )
if isinstance( dict_state, dict ):
args['features'] = dict_state
except Exception as e:
self.logger.debug(e)


@cherrypy.expose
Expand All @@ -625,15 +674,6 @@ def login(self):
args = cherrypy.request.json
# can raise exception
(auth, user ) = self.validate_env()
# check if args contains a features dict
if args.get('features') is not None :
if isinstance( args.get('features'), dict ) :
# this request asks for custom features
# Check if features update are allowed
if 'submit' not in oc.od.settings.desktop.get['features_permissions']:
raise cherrypy.HTTPError(401, message="'submit' is not in desktop.features_permissions, update configuration file" )
else:
raise cherrypy.HTTPError(401, message="bad parameters features, features must be a dict" )

# push a start message to database cache info
services.messageinfo.start( user.userid, "b.Launching desktop")
Expand Down
98 changes: 49 additions & 49 deletions oc/auth/authservice.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import datetime
import re
import threading
import base64
from urllib.parse import urlparse
from ldap import filter as ldap_filter
import ldap3
Expand Down Expand Up @@ -1543,30 +1544,23 @@ def mesuretimeserver_auth_duration( self,server_utctimestamp):
auth_duration_in_milliseconds = (server_endoflogin_utctimestamp - server_utctimestamp)/1000 # in float second
return auth_duration_in_milliseconds

def update_user_resqueted_executeclassname(self, auth, **arguments)->None:
# execute class is defined separatly
# if an executeclassname is already defined by rules
# do not read the users requested value
# rules preempt the executeclassname value

user_requested_features = arguments.get('features')
def update_user_resqueted_executeclassname(self, auth, user_requested_features:dict)->None:
# update auth.data['labels'] with user_requested_features entries
if not isinstance( user_requested_features ,dict ):
return

executeclassname = user_requested_features.get('executeclassname')
if not isinstance( executeclassname, str) :
return

# executeclassname is a str
if auth.data['labels'].get('executeclassname') is None:
# look for users requested arguments
self.logger.debug( f"user asks for executeclassname={executeclassname}" )
auth.data['labels']['executeclassname'] = executeclassname
else:
self.logger.warning(
f"conflit executeclassname is already defined {auth.data['labels'].get('executeclassname')} \
but user requested value {arguments.get('executeclassname')}"
# filter to ['executeclassname']
for feature_name in ['executeclassname'] :
feature_value = user_requested_features.get(feature_name)
if not isinstance( feature_value, str) :
return

self.logger.debug(
f"previous auth.data['labels']['{feature_name}']={auth.data['labels'].get(feature_name)} updating value to auth.data['labels']['{feature_name}']='{feature_value}'"
)

auth.data['labels'][feature_name] = feature_value


def login(self, provider, manager=None, **arguments):
self.logger.debug('')
Expand All @@ -1586,39 +1580,39 @@ def login(self, provider, manager=None, **arguments):

# look for an auth manager
mgr = self.findmanager(provider, manager)

# get the provider object from the provider name
pdr = mgr.getprovider(provider, raise_error=True)

# do authenticate with the auth manager
self.logger.debug( f"mgr.authenticate provider={provider} start")
auth = mgr.authenticate(provider, **arguments)
self.logger.debug( f"mgr.authenticate provider={provider} done")
self.logger.debug( f"pdr.authenticate provider={provider} start")
auth = pdr.authenticate( **arguments)
self.logger.debug( f"pdr.authenticate provider={provider} done")

if not isinstance( auth, AuthInfo ):
raise AuthenticationFailureError('No authentication provided')

# uncomment this line only to dump password in clear text format
# self.logger.debug( f"mgr.getuserinfo arguments={arguments}")
self.logger.debug( f"mgr.getuserinfo provider={provider} start")
userinfo = mgr.getuserinfo(provider, auth, **arguments)
self.logger.debug( f"mgr.getuserinfo provider={provider} done")
self.logger.debug( f"pdr.getuserinfo provider={provider} start")
userinfo = pdr.getuserinfo( auth, **arguments)
self.logger.debug( f"pdr.getuserinfo provider={provider} done")
if not isinstance(userinfo, dict ):
raise AuthenticationFailureError(f"getuserinfo return {type(userinfo)} provider={provider}")

#
# create claims with auth and userinfo
self.logger.debug( f"mgr.createclaims provider={provider} start")
mgr.createclaims(provider, auth, userinfo, **arguments )
self.logger.debug( f"mgr.createclaims provider={provider} done")
self.logger.debug( f"pdr.createclaims provider={provider} start")
pdr.createclaims( auth, userinfo, **arguments )
self.logger.debug( f"pdr.createclaims provider={provider} done")

#
# get roles
self.logger.debug( f"mgr.getroles provider={provider} start")
roles = mgr.getroles(provider, auth, userinfo, **arguments)
self.logger.debug( f"mgr.getroles provider={provider} done")
self.logger.debug( f"pdr.getroles provider={provider} start")
roles = pdr.getroles( auth, userinfo, **arguments)
self.logger.debug( f"pdr.getroles provider={provider} done")
if not isinstance(roles, list):
raise AuthenticationFailureError( f"mgr.getroles provider={provider} error" )

# get the provider object from the provider name
pdr = mgr.getprovider(provider)
raise AuthenticationFailureError( f"pdr.getroles provider={provider} error" )

# check if acl matches with tag
if not oc.od.acl.ODAcl().isAllowed( auth, pdr.acls ):
Expand All @@ -1632,7 +1626,7 @@ def login(self, provider, manager=None, **arguments):

# update auth.data['labels']['executeclassname']
# if user requests feature executeclassname
self.update_user_resqueted_executeclassname( auth, **arguments)
self.update_user_resqueted_executeclassname( auth, arguments.get('features') )


# dump labels for debug
Expand All @@ -1654,9 +1648,7 @@ def login(self, provider, manager=None, **arguments):
reason=reason )

finally:
# call finalize to clean conn if need
if isinstance( auth, AuthInfo ) :
mgr.finalize(provider, auth, **arguments)
pdr.finalize( auth, **arguments)

return response

Expand Down Expand Up @@ -1857,6 +1849,7 @@ def createprovider(self, name, config):
provider = ODLdapAuthProvider(self, name, config)
return provider


def isActiveDirectory( self, config ) -> bool:
"""[isActiveDirectory]
True if config is an ActiveDirectory config else False
Expand Down Expand Up @@ -2141,7 +2134,7 @@ def createauthenv(self, userinfo, userid, password):
def createclaims( self, authinfo, userinfo, **arguments):
userid = self.default_user_if_not_exist
password = self.default_passwd_if_not_exist
claims = { 'identity': self.createauthenv(userinfo, userid, password) }
claims = { 'identity': self.createauthenv(userinfo, userid, password) }
authinfo.set_claims(claims)

# Implement OAuth 2.0 AuthProvider
Expand All @@ -2160,7 +2153,8 @@ def __init__(self, manager, name, config):
self.userinfo_auth = config.get('userinfo_auth', False) is True
self.type = config.get('type', 'oauth')
self.userinfomap = config.get('userinfomap')

self.state = config.get('state')

self.authorization_base_url = config.get('authorization_base_url')
self.token_url = config.get('token_url')
self.redirect_uri_prefix = config.get('redirect_uri_prefix')
Expand All @@ -2174,21 +2168,22 @@ def __init__(self, manager, name, config):
# the defautl attribut name for memberof is groups
self.memberof_attribut_name = config.get('memberof_attribut_name', 'groups')


def getclientdata(self):
data = super().getclientdata()
oauthsession = OAuth2Session( self.client_id, scope=self.scope, redirect_uri=self.redirect_uri)
authorization_url, state = oauthsession.authorization_url( self.authorization_base_url )
data['dialog_url'] = authorization_url
authorization_url, state = oauthsession.authorization_url( self.authorization_base_url, state=self.state )
data['dialog_url'] = authorization_url
data['explicitproviderapproval'] = self.explicitproviderapproval
data['state'] = state
data['state'] = state
return data

def authenticate(self, code=None, **params):
oauthsession = OAuth2Session( self.client_id, scope=self.scope, redirect_uri=self.redirect_uri)
authorization_response = self.redirect_uri_prefix + '?' + cherrypy.request.query_string
token = oauthsession.fetch_token( self.token_url, client_secret=self.client_secret, authorization_response=authorization_response )
self.logger.debug( f"provider {self.name} type {self.type} return token {token}" )
authinfo = AuthInfo( provider=self.name, providertype=self.type, token=oauthsession, protocol='oauth')
authinfo = AuthInfo( provider=self.name, providertype=self.type, token=oauthsession, protocol='oauth', data={})
return authinfo


Expand Down Expand Up @@ -2242,12 +2237,15 @@ def parseuserinfo(self, jsondata:dict)->dict:
user['name'] = name
return user

def finalize(self, authinfo, **params):
def finalize(self, auth, **params):
if not isinstance( auth, AuthInfo ):
return
# retrieve the token object from the previous authinfo
oauthsession = authinfo.token
oauthsession = auth.token
# Check if type is OAuth2Session
if isinstance(oauthsession,OAuth2Session) :
authinfo.token = oauthsession.token
auth.token = oauthsession.token
oauthsession.close()

def getroles(self, authinfo, userinfo, **params):
self.logger.debug('')
Expand Down Expand Up @@ -2620,6 +2618,8 @@ def issafeLdapAuthCommonName(cn):
return True

def finalize( self, auth, **params):
if not isinstance( auth, AuthInfo ):
return
if isinstance( auth.conn , ldap3.core.connection.Connection ):
try:
auth.conn.unbind()
Expand Down

0 comments on commit 1867f24

Please sign in to comment.