1
0
mirror of https://github.com/dkmstr/openuds.git synced 2025-01-03 01:17:56 +03:00

Started a REST api for UDS.

The idea is provide third party access to UDS, and try to use this as an starting point to make an web based administration, so we can deprecate .net administration tool (it is becoming an annoyance... :-) ), and web based admin is always more accesible than thin client admin
This commit is contained in:
Adolfo Gómez 2013-11-11 19:38:23 +00:00
parent 918259079a
commit 47dff34637
13 changed files with 524 additions and 13 deletions

View File

@ -1,8 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<?eclipse-pydev version="1.0"?> <?eclipse-pydev version="1.0"?><pydev_project>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">Default</pydev_property>
<pydev_project>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">python2.7</pydev_property>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python 2.7</pydev_property> <pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python 2.7</pydev_property>
<pydev_variables_property name="org.python.pydev.PROJECT_VARIABLE_SUBSTITUTION"> <pydev_variables_property name="org.python.pydev.PROJECT_VARIABLE_SUBSTITUTION">
<key>DJANGO_MANAGE_LOCATION</key> <key>DJANGO_MANAGE_LOCATION</key>

View File

@ -8,6 +8,10 @@ encoding//documentation/_downloads/samples/services/__init__.py=utf-8
encoding//documentation/conf.py=utf-8 encoding//documentation/conf.py=utf-8
encoding//src/server/settings.py=utf-8 encoding//src/server/settings.py=utf-8
encoding//src/server/urls.py=utf-8 encoding//src/server/urls.py=utf-8
encoding//src/uds/REST/__init__.py=utf-8
encoding//src/uds/REST/handlers.py=utf-8
encoding//src/uds/REST/methods/authentication.py=utf-8
encoding//src/uds/REST/processors.py=utf-8
encoding//src/uds/__init__.py=utf-8 encoding//src/uds/__init__.py=utf-8
encoding//src/uds/auths/ActiveDirectory/Authenticator.py=utf-8 encoding//src/uds/auths/ActiveDirectory/Authenticator.py=utf-8
encoding//src/uds/auths/ActiveDirectory/__init__.py=utf-8 encoding//src/uds/auths/ActiveDirectory/__init__.py=utf-8

1
server/requirements.txt Normal file
View File

@ -0,0 +1 @@
django-compressor

View File

@ -2,8 +2,7 @@
''' '''
Url patterns for UDS project (Django) Url patterns for UDS project (Django)
''' '''
from django.conf.urls import patterns, include
from django.conf.urls.defaults import patterns, include
# Uncomment the next two lines to enable the admin: # Uncomment the next two lines to enable the admin:
# from django.contrib import admin # from django.contrib import admin

View File

@ -0,0 +1,175 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012 Virtual Cable S.L.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of Virtual Cable S.L. nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
'''
@author: Adolfo Gómez, dkmaster at dkmon dot com
'''
from __future__ import unicode_literals
from django import http
from django.views.generic.base import View
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from django.utils.translation import ugettext as _
from django.conf import settings
from handlers import Handler, HandlerError, AccessDenied
import logging
logger = logging.getLogger(__name__)
__all__ = [ str(v) for v in ['Handler', 'Dispatcher'] ]
class Dispatcher(View):
services = { '' : None } # Will include a default /rest handler, but rigth now this will be fine
@method_decorator(csrf_exempt)
def dispatch(self, request, **kwargs):
import processors
# Remove session, so response middelwares do nothing with this
del request.session
# Now we extract method and posible variables from path
path = kwargs['arguments'].split('/')
del kwargs['arguments']
# Transverse service nodes too look for path
service = Dispatcher.services
full_path = []
# Last element will be
do_break = False
cls = None
while len(path) > 0 and not do_break:
# .json, .xml, ... will break path recursion
do_break = path[0].find('.') != -1
clean_path = path[0].split('.')[0]
if service.has_key(clean_path):
service = service[clean_path]
full_path.append(path[0])
path = path[1:]
else:
break
full_path = '/'.join(full_path)
logger.debug(full_path)
cls = service['']
if cls is None:
return http.HttpResponseNotFound('method not found')
# Guess content type from content type header or ".xxx" to method
try:
p = full_path.split('.')
processor = processors.available_processors_ext_dict[p[1]](request)
except:
processor = processors.available_processors_mime_dict.get(request.META['CONTENT_TYPE'], processors.default_processor)(request)
# Obtain method to be invoked
http_method = request.method.lower()
args = path
# Inspect
lang = None
if len(args) > 0:
for l in settings.LANGUAGES:
if args[-1] == l[0]:
lang = l[0]
logger.error('Found lang {0}'.format(l))
args = args[:-1]
break
# Intantiate method handler and locate http_method dispatcher
try:
handler = cls(request, full_path, http_method, processor.processParameters(), *args, **kwargs)
operation = getattr(handler, http_method)
except processors.ParametersException as e:
return http.HttpResponseServerError('Invalid parameters invoking {0}: {1}'.format(path[0], e))
except AttributeError:
allowedMethods = []
for n in ['get', 'post', 'put', 'delete']:
if hasattr(handler, n):
allowedMethods.append(n)
return http.HttpResponseNotAllowed(allowedMethods)
except AccessDenied:
return http.HttpResponseForbidden('method access denied')
except:
logger.exception('error accessing attribute')
logger.debug('Getting attribute {0} for {1}'.format(http_method, full_path))
return http.HttpResponseServerError('Unexcepected error')
# Invokes the handler's operation, add headers to response and returns
try:
response = processor.getResponse(operation())
for k, v in handler.headers().iteritems():
response[k] = v
return response
except HandlerError as e:
return http.HttpResponseBadRequest(unicode(e))
except Exception as e:
logger.exception('Error processing request')
return http.HttpResponseServerError(unicode(e))
# Initializes the dispatchers
@staticmethod
def initialize():
'''
This imports all packages that are descendant of this package, and, after that,
it register all subclases of service provider as
'''
import os.path, pkgutil
import sys
# Dinamycally import children of this package. The __init__.py files must register, if needed, inside ServiceProviderFactory
package = 'methods'
pkgpath = os.path.join(os.path.dirname(sys.modules[__name__].__file__), package)
for _, name, _ in pkgutil.iter_modules([pkgpath]):
__import__(__name__ + '.' + package + '.' + name, globals(), locals(), [], -1)
for cls in Handler.__subclasses__(): # @UndefinedVariable
# Skip ClusteredServiceProvider
if cls.name is None:
name = cls.__name__.lower()
else:
name = cls.name
logger.debug('Adding handler {0} for method {1} in path {2}'.format(cls, name, cls.path))
service_node = Dispatcher.services
if cls.path is not None:
for k in cls.path.split('/'):
if service_node.get(k) is None:
service_node[k] = { '' : None }
service_node = service_node[k]
if service_node.get(name) is None:
service_node[name] = { '' : None }
service_node[name][''] = cls
Dispatcher.initialize()

View File

@ -0,0 +1,132 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012 Virtual Cable S.L.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of Virtual Cable S.L. nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
'''
@author: Adolfo Gómez, dkmaster at dkmon dot com
'''
from __future__ import unicode_literals
from django.contrib.sessions.backends.db import SessionStore
from django.utils.translation import activate
from django.conf import settings
from uds.core.util.Config import GlobalConfig
import logging
logger = logging.getLogger(__name__)
AUTH_TOKEN_HEADER = 'HTTP_X_AUTH_TOKEN'
class HandlerError(Exception):
pass
class AccessDenied(HandlerError):
pass
class Handler(object):
name = None # If name is not used, name will be the class name in lower case
path = None # Path for this method, so we can do /auth/login, /auth/logout, /auth/auths in a simple way
authenticated = True # By default, all handlers needs authentication
only_admin = False # By default, the methods will be accesible by anyone
# method names: 'get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'
def __init__(self, request, path, operation, params, *args, **kwargs):
if self.only_admin:
self.authenticated = True # If only_admin, must also be authenticated
self._request = request
self._path = path
self._operation = operation
self._params = params
self._args = args
self._kwargs = kwargs
self._headers = {}
self._authToken = None
if self.authenticated: # Only retrieve auth related data on authenticated handlers
try:
self._authToken = self._request.META.get(AUTH_TOKEN_HEADER, '')
self._session = SessionStore(session_key = self._authToken)
if not self._session.has_key('REST'):
raise Exception() # No valid session, so auth_token is also invalid
except:
if settings.DEBUG:
if self._authToken == 'a':
self.genAuthToken(-1, 'root', 'es', True)
else:
self._authToken = None
self._session = None
if self._authToken is None:
raise AccessDenied()
if self.only_admin and not self.getValue('is_admin'):
raise AccessDenied()
def headers(self):
return self._headers
def header(self, header_):
return self._headers.get(header_)
def addHeader(self, header, value):
self._headers[header] = value
def removeHeader(self, header):
try:
del self._headers[header]
except:
pass
# Auth related
def getAuthToken(self):
return self._authToken
def genAuthToken(self, id_auth, username, locale, is_admin):
session = SessionStore()
session.set_expiry(GlobalConfig.ADMIN_IDLE_TIME.getInt())
session['REST'] = { 'auth': id_auth, 'username': username, 'locale': locale, 'is_admin': is_admin }
session.save()
self._authToken = session.session_key
self._session = session
return self._authToken
def cleanAuthToken(self):
self._authToken = None
if self._session:
self._session.delete()
self._session = None
# Session related (from auth token)
def getValue(self, key):
try:
return self._session['REST'].get(key)
except:
return None

View File

View File

@ -0,0 +1,105 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012 Virtual Cable S.L.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of Virtual Cable S.L. nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
'''
@author: Adolfo Gómez, dkmaster at dkmon dot com
'''
from __future__ import unicode_literals
from uds.core.util.Config import GlobalConfig
from uds.models import Authenticator, User
from uds.REST import Handler, HandlerError
import logging
logger = logging.getLogger(__name__)
class Login(Handler):
path = 'auth'
authenticated = False # By default, all handlers needs authentication
admin_method = False # By default, the methods will be accesible by anyone
def post(self):
'''
This login uses parameters to generate auth token
The alternative is to use the template tag inside "REST" that is called auth_token, that extracts an auth token from an user session
We can use any of this forms due to the fact that the auth token is in fact a session key
Parameters:
mandatory:
username:
password:
auth:
optional:
locale: (defaults to "en")
Result:
on success: { 'result': 'ok', 'auth': [auth_code] }
on error: { 'result: 'error', 'error': [error string] }
'''
try:
username, auth, password = self._params['username'], self._params['auth'], self._params['password']
locale = self._params.get('locale', 'en')
if auth == 'admin':
if GlobalConfig.SUPER_USER_LOGIN.get(True) == username and GlobalConfig.SUPER_USER_PASS.get(True) == password:
self.genAuthToken(-1, username, locale, True)
return{'result': 'ok', 'token': self.getAuthToken()}
else:
raise Exception('Invalid credentials')
except Exception as e:
logger.exception('exception')
return {'result': 'error', 'error': unicode(e)}
class Logout(Handler):
path = 'auth'
authenticated = True # By default, all handlers needs authentication
admin_method = False # By default, the methods will be accesible by anyone
def get(self):
# Remove auth token
self.cleanAuthToken()
return 'done'
def post(self):
return self.get()
class Auth(Handler):
authenticated = False # By default, all handlers needs authentication
admin_method = False # By default, the methods will be accesible by anyone
def auths(self):
for a in Authenticator.all():
if a.getType().isCustom() is False:
yield { 'auth' : str(a.small_name), 'name' : a.name }
def get(self):
return list(self.auths())

View File

@ -0,0 +1,98 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012 Virtual Cable S.L.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of Virtual Cable S.L. nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
'''
@author: Adolfo Gómez, dkmaster at dkmon dot com
'''
from __future__ import unicode_literals
from django.utils import simplejson
from django import http
import logging
logger = logging.getLogger(__name__)
class ParametersException(Exception):
pass
class ContentProcessor(object):
mime_type = None
extensions = None
def __init__(self, request):
self._request = request
def processParameters(self):
return ''
def getResponse(self, obj):
return http.HttpResponse(content = self.render(obj), content_type=self.mime_type + "; charset=utf-8")
def render(self, obj):
return unicode(obj)
# ---------------
# Json Processor
# ---------------
class JsonProcessor(ContentProcessor):
mime_type = 'application/json'
extensions = ['json']
def processParameters(self):
try:
if len(self._request.body) == 0:
return {}
res = simplejson.loads(self._request.body)
logger.debug(res)
return res
except Exception as e:
logger.error('parsing json: {0}'.format(e))
raise ParametersException(unicode(e))
def render(self, obj):
return simplejson.dumps(obj)
# ---------------
# Json Processor
# ---------------
class XMLProcessor(ContentProcessor):
mime_type = 'application/xml'
extensions = ['xml']
def processParameters(self):
return ''
processors_list = (JsonProcessor,XMLProcessor)
default_processor = JsonProcessor
available_processors_mime_dict = dict((cls.mime_type, cls) for cls in processors_list)
available_processors_ext_dict = {}
for cls in processors_list:
for ext in cls.extensions:
available_processors_ext_dict[ext] = cls

View File

@ -31,7 +31,7 @@
@author: Adolfo Gómez, dkmaster at dkmon dot com @author: Adolfo Gómez, dkmaster at dkmon dot com
''' '''
from django.conf.urls.defaults import patterns, include from django.conf.urls import patterns
urlpatterns = patterns(__package__, urlpatterns = patterns(__package__,
(r'^guacamole/(?P<tunnelId>.+)$', 'views.guacamole'), (r'^guacamole/(?P<tunnelId>.+)$', 'views.guacamole'),

View File

@ -31,7 +31,7 @@
@author: Adolfo Gómez, dkmaster at dkmon dot com @author: Adolfo Gómez, dkmaster at dkmon dot com
''' '''
from django.conf.urls.defaults import patterns, include from django.conf.urls import patterns
urlpatterns = patterns(__package__, urlpatterns = patterns(__package__,
(r'^pam$', 'views.pam'), (r'^pam$', 'views.pam'),

View File

@ -1,5 +1,4 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Copyright (c) 2012 Virtual Cable S.L. # Copyright (c) 2012 Virtual Cable S.L.
# All rights reserved. # All rights reserved.
@ -32,8 +31,9 @@
''' '''
from django.conf import settings from django.conf import settings
from django.conf.urls.defaults import patterns, include from django.conf.urls import patterns, include
from uds.core.util.modfinder import loadModulesUrls from uds.core.util.modfinder import loadModulesUrls
from uds import REST
urlpatterns = patterns('uds', urlpatterns = patterns('uds',
(r'^$', 'web.views.index'), (r'^$', 'web.views.index'),
@ -64,8 +64,7 @@ urlpatterns = patterns('uds',
(r'^authJava/(?P<idAuth>.+)/(?P<hasJava>.*)$', 'web.views.authJava'), (r'^authJava/(?P<idAuth>.+)/(?P<hasJava>.*)$', 'web.views.authJava'),
(r'^authinfo/(?P<authName>.+)', 'web.views.authInfo'), (r'^authinfo/(?P<authName>.+)', 'web.views.authInfo'),
(r'^about', 'web.views.about'), (r'^about', 'web.views.about'),
(r'^about', 'web.views.about'), (r'^rest/(?P<arguments>.*)$', REST.Dispatcher.as_view()),
) )
# Append urls from special dispatcher # Append urls from special dispatcher

View File

@ -76,7 +76,7 @@ class XMLRPCDispatcher(SimpleXMLRPCDispatcher):
def dispatch(self, request, **kwargs): def dispatch(self, request, **kwargs):
import xmlrpclib import xmlrpclib
xml = request.raw_post_data xml = request.body
try: try:
params, method = xmlrpclib.loads(xml) params, method = xmlrpclib.loads(xml)
try: try: