"""
VOSI renderers.
These are really three different renderers for each service. IVOA wants
it this way (in effect, since they are supposed to be three different
capabilities).
"""
#c Copyright 2008-2023, the GAVO project <gavo@ari.uni-heidelberg.de>
#c
#c This program is free software, covered by the GNU GPL. See the
#c COPYING file in the source distribution.
from twisted.internet import threads
from twisted.web import resource
from twisted.web import server
from gavo import base
from gavo import registry
from gavo import svcs
from gavo import utils
from gavo.base import meta
from gavo.registry import capabilities
from gavo.registry import tableset
from gavo.registry.model import VS
from gavo.protocols import creds
from gavo.utils.stanxml import Element, registerPrefix, schemaURL, xsiPrefix
from gavo.web import grend
from gavo.web import weberrors
registerPrefix("avl", "http://www.ivoa.net/xml/VOSIAvailability/v1.0",
schemaURL("VOSIAvailability-v1.0.xsd"))
registerPrefix("cap", "http://www.ivoa.net/xml/VOSICapabilities/v1.0",
schemaURL("VOSICapabilities-v1.0.xsd"))
registerPrefix("vtm", "http://www.ivoa.net/xml/VOSITables/v1.0",
schemaURL("VOSITables-v1.0.xsd"))
[docs]class VOSIRenderMixin(object):
"""A mixin furnishing the basic rendering methods for VOSI resources.
You will have to provide a method _getTree(request) returning some
xmlstan.
"""
[docs] def render(self, request):
request.setHeader("content-type", "text/xml")
threads.deferToThread(self._getTree, request
).addCallback(self._shipout, request
).addCallback(request.finishCallback
).addErrback(weberrors.renderDCErrorPage, request)
return server.NOT_DONE_YET
def _shipout(self, response, request):
utils.xmlwrite(response, request,
prolog="<?xml-stylesheet href='/static/xsl/vosi.xsl' type='text/xsl'?>")
[docs]class VOSIRenderer(VOSIRenderMixin, grend.ServiceBasedPage):
"""An abstract base for renderers handling VOSI requests.
All of these return some sort of XML and are legal on all services.
The actual documents returned are defined in _getTree(request)->deferred
firing stanxml.
"""
checkedRenderer = False
def _getTree(self, request): # pragma: no cover
raise ValueError("_getTree has not been overridden.")
############ The VOSI data models (no better place for it yet)
[docs]class AVL(object):
"""The container for elements from the VOSI availability schema.
"""
[docs] class AVLElement(Element):
_prefix = "avl"
[docs] class availability(AVLElement):
_additionalPrefixes = xsiPrefix
[docs] class available(AVLElement): pass
[docs] class upSince(AVLElement): pass
[docs] class downAt(AVLElement): pass
[docs] class backAt(AVLElement): pass
[docs] class note(AVLElement): pass
[docs]class CAP(object):
"""The container for element from the VOSI capabilities schema.
"""
[docs] class CAPElement(Element):
_prefix = "cap"
[docs] class capabilities(CAPElement):
_mayBeEmpty = True
[docs]class VTM(object):
"""The container for element from the VOSI tableset schema.
"""
[docs] class VTMElement(Element):
_prefix = "vtm"
[docs] class tableset(VTMElement):
_mayBeEmpty = True
[docs] class table(VTMElement, VS.table):
_mayBeEmpty = True
_local = False
SF = meta.stanFactory
_availabilityBuilder = meta.ModelBasedBuilder([
('available', SF(AVL.available)),
('upSince', SF(AVL.upSince)),
('_scheduledDowntime', SF(AVL.downAt)),
('backAt', SF(AVL.backAt)),
('availability_note', SF(AVL.note)),
('crash_this', lambda: "invalid, for testing"),
])
############ The actual VOSI renderers
[docs]class VOSIAvailabilityRenderer(VOSIRenderer):
"""A renderer for a VOSI availability endpoint.
An endpoint with this renderer is automatically registered for
every service. The answers can be configured using the admin
renderer.
"""
name = "availability"
def _getTree(self, request):
root = AVL.availability[
_availabilityBuilder.build(self.service)]
return root
[docs]class VOSICapabilityRenderer(VOSIRenderer):
"""A renderer for a VOSI capability endpoint.
An endpoint with this renderer is automatically registered for
every service. The responses contain information on what renderers
("interfaces") are available for a service and what properties they have.
This also doubles as a canary for authentication, which is why there
are the somewhat complicated things in render; cf.
https://wiki.ivoa.net/twiki/bin/view/IVOA/SSO_next
"""
name = "capabilities"
def _getTree(self, request):
root = CAP.capabilities()
for pub in self.service.getPublicationsForSet(None):
try:
root = root[capabilities.getCapabilityElement(pub)]
except Exception as msg:
base.ui.notifyError("Error while creating VOSI capability"
" for %s: %s"%(
self.service.getURL(pub.render, absolute=False),
msg))
return root
[docs] def render(self, request):
if request.getUser():
return self._renderWithUser(request)
else:
return self._renderWithoutUser(request)
def _addChallenge(self, request):
"""adds a www-authenticate header to request.
"""
# We may have to figure out a way to admit different realms here,
# but for now just use our global realm
realm = base.getConfig("web", "realm")
request.setHeader('WWW-Authenticate',
'Basic realm="%s"'%realm)
def _renderWithUser(self, request):
"""creates a response when a request contains some credentials.
Four cases:
* bad credentials: return a 401 with an auth challenge.
* open service, good credentials: return 200 with the authenticated id
* closed service, good credentials: return 200 with the authenticated id
* closed service, good credentials but not authorised:
return 401 with a challenge.
"""
groups = creds.getGroupsForUser(request.getUser(), request.getPassword())
if self.service.limitTo is None:
isAuth = bool(groups)
else:
isAuth = self.service.limitTo in groups
request.setHeader("content-type", "text/plain")
if isAuth:
request.setHeader("x-vo-authenticated", request.getUser())
else:
self._addChallenge(request)
request.setResponseCode(401)
return super().render(request)
def _renderWithoutUser(self, request):
"""creates a response when a request does not contain credentials.
Two cases:
* open service: return a 200 but announce optional auth for the
sake of services that may support gratuitous auth.
* closed service: return 401 and a challenge.
"""
self._addChallenge(request)
if self.service.limitTo is not None:
request.setResponseCode(401)
return super().render(request)
[docs]class VOSITableResponse(VOSIRenderMixin, resource.Resource):
"""A resource building a VOSI tableset for a single table.
This is returned as a child resource of VOSITablesetRenderer.
"""
def __init__(self, tableDef):
self.tableDef = tableDef
resource.Resource.__init__(self)
def _getTree(self, request):
return tableset.getTableForTableDef(self.tableDef, [],
rootElement=VTM.table)
[docs] def getChild(self, request, name):
raise svcs.UnknownURI("VOSI tables resources have no children")
[docs]class VOSITablesetRenderer(VOSIRenderer):
"""A renderer for a VOSI table metadata endpoint.
An endpoint with this renderer is automatically registered for
every service. The responses contain information on the tables
exposed by a given service.
"""
name = "tableMetadata"
aliases = frozenset(["tables"])
def _getTree(self, request):
detail = request.strargs.get("detail",
[base.getConfig("ivoa", "vositabledetail")])[0]
root = registry.getTablesetForService(self.service,
rootElement=VTM.tableset, suppressBodies=detail=="min")
return root
[docs] def getChild(self, name, request):
if name==b"":
raise svcs.WebRedirect(self.service.getURL("tableMetadata"))
tableName = name.decode("utf-8").lower()
for td in self.service.getTableSet():
if td.getQName().lower()==tableName:
return VOSITableResponse(td)
raise svcs.UnknownURI("No table %s on this service."%tableName)