SciTokens Library
=================
|pypi| |downloads| |license|
This library aims to be a reference implementation of the SciTokens'
JSON Web Token (JWT) token format.
SciTokens is built on top of the
`PyJWT <https://github.com/jpadilla/pyjwt>`__ and
`cryptography <https://cryptography.io/en/latest/>`__ libraries. We aim
to provide a safe, high-level interface for token manipulation, avoiding
common pitfalls of using the underling libraries directly.
*NOTE*: SciTokens (the format and this library) is currently being
designed; this README describes how we would like it to work, not
necessarily current functionality. Particularly, we do not foresee the
chained tokens described here as part of the first release's
functionality. The ideas behind the separate ``Validator`` in this
library is taken from
`libmacaroons <https://github.com/rescrv/libmacaroons>`__.
Generating Tokens
-----------------
Usage revolves around the ``SciToken`` object. This can be generated
directly:
::
>>> import scitokens
>>> token = scitokens.SciToken() # Create token and generate a new private key
>>> token2 = scitokens.SciToken(key=private_key) # Create token using existing key
where ``key`` is a private key object (more later on generating private
keys). Direct generation using a private key will most often be done to
do a *base token*. SciTokens can be chained, meaning one token can be
appended to another:
::
>>> token = scitokens.SciToken(parent=parent_token)
The generated object, ``token``, will default to having all the
authoriations of the parent token - but is mutable and can add further
restrictions.
Tokens contain zero or more claims, which are facts about the token that
typically indicate some sort of authorization the bearer of the token
has. A token has a list of key-value pairs; each token can only have a
single value per key, but multiple values per key can occur in a token
chain.
To set a claim, one can use dictionary-like setter:
::
>>> token['claim1'] = 'value2'
The value of each claim should be a Python object that can be serialized
to JSON.
Token Serialization
-------------------
Parent tokens are typically generated by a separate server and sent as a
response to a successful authentication or authorization request.
SciTokens are built on top of JSON Web Tokens (JWT), which define a
useful base64-encoded serialization format. A serialized token may look
something like this:
::
eyJhbGciOiJFUzI1NiIsImN3ayI6eyJ5IjoiazRlM1FFeDVjdGJsWmNrVkhINlkzSFZoTzFadUxVVWNZQW5ON0xkREV3YyIsIngiOiI4TkU2ZEE2T1g4NHBybHZEaDZUX3kwcWJOYmc5a2xWc2pYQnJnSkw5aElBIiwiY3J2IjoiUC0yNTYiLCJrdHkiOiJFQyJ9LCJ0eXAiOiJKV1QiLCJ4NXUiOiJodHRwczovL3ZvLmV4YW1wbGUuY29tL0pXUyJ9.eyJyZWFkIjoiL2xpZ28ifQ.uXVzbcOBCK4S4W89HzlWNmnE9ZcpuRHKTrTXYv8LZL9cDy3Injf97xNPm756fKcYwBO5KykYngFrUSGa4owglA.eyJjcnYiOiAiUC0yNTYiLCAia3R5IjogIkVDIiwgImQiOiAieWVUTTdsVXk5bGJEX2hnLVVjaGp0aXZFWHZxSWxoelJQVEVaZDBaNFBpOCJ9
This is actually 4 separate base64-encoded strings, separated by the
``.`` character. The four pieces are:
- A *header*, implementing the JSON Web Key standard, specifying the
cryptographic properties of the token.
- A *payload*, specifying the claims (key-value pairs) encoded by the
token and asserted by the VO.
- A *signature* of the header and payload, ensuring authenticity of the
payload.
- A *key*, utilized to sign any derived tokens. The key is an optional
part of the token format, but may be required by some remote
services.
Given a serialized token, the ``scitokens`` library can deserialize it:
::
>>> token = scitokens.SciToken.deserialize(token_serialized_bytes)
As part of the deserialization, the ``scitokens`` library will throw an
exception if token verification failed.
The existing token can be serialized with the ``serialize`` method:
::
>>> token_serialized_bytes = token.serialize()
Validating Tokens
-----------------
In SciTokens, we try to distinguish between *validating* and *verifying*
tokens. Here, verification refers to determining the integrity and
authenticity of the token: can we validate the token came from a known
source without tampering? Can we validate the chain of trust? Validation
is determining whether the claims of the token are satisfied in a given
context.
For example, if a token contains the claims
``{vo: ligo, op: read, path: /ligo}``, we would first verify that the
token is correctly signed by a known public key associated with LIGO.
When presented to a storage system along with an HTTP request, the
storage system would validate the token authorizes the corresponding
request (is it a GET request? Is it for a sub-path of /ligo?).
Within the ``scitokens`` module, validation is done by the ``Validator``
object:
::
>>> val = scitokens.Validator()
This object can be reused for multiple validations. All SciToken claims
must be validated. There are no "optional" claim attributes or values.
To validate a specific claim, provide a callback function to the
``Validator`` object:
::
>>> def validate_op(value):
... return value == True
>>> val.add_validator("op", validate_op)
Once all the known validator callbacks have been registered, use the
``validate`` method with a token:
::
>>> val.validate(token)
This will throw a ``ValidationException`` if the token could not be
validated.
Enforcing SciTokens Logic
-------------------------
For most users of SciTokens, determining that a token is valid is insufficient.
Rather, most will be asking "does this token allow the current resource
request?" The valid token must be compared to some action the user is
attempting to take.
To assist in the authorization enforcement, the SciTokens library provides
the ``Enforcer`` class.
An unique Enforcer object is needed for each thread and issuer:
::
>>> enf = scitokens.Enforcer("https://scitokens.org/dteam")
This object will accept tokens targetted to any audience; a more typical
use case will look like the following:
::
>>> enf = scitokens.Enforcer("https://scitokens.org/dteam",
audience="https://example.com")
This second enforcer would not accept tokens that are intended for
https://google.com.
The enforcer can then test authorization logic against a valid token:
::
>>> token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtp..."
>>> stoken = scitokens.SciToken.deserialize(token)
>>> enf.generate_acls(stoken)
[(u'write', u'/store/user/bbockelm'), (u'read', u'/store')]
>>> enf.test(stoken, "read", "/store/foo")
True
>>> enf.test(stoken, "write", "/store/foo")
False
>>> enf.test(stoken, "write", "/store/user/foo")
False
>>> enf.test(stoken, "write", "/store/user/bbockelm/foo")
True
The ``test`` method uses the SciTokens built-in path parsing to validate the
authorization. The ``generate_acls`` method allows the caller to cache
the ACL information from the token.
Creating Sample Tokens
----------------------
Typically, an access token is generated during an OAuth2 workflow to facilitate
authentication and authorization. However, for testing and experimentation purposes,
`the demo token generator <https://demo.scitokens.org/>`__ provides users with the
ability to create sample tokens with customized payload:
::
>>> payload = {"sub": "<email adress>", "scope": "read:/protected"}
>>> token = scitokens.utils.demo.token(payload)
The ``token`` method makes a request to the generator to create a serialized token
for the specified payload. Users can also retrieve a parsed token by calling the
``parsed_token`` method, which returns a SciToken object corresponding to the
token. The object contains the decoded token data, including the claims and signature.
Decorator
-------------
This protect decorator is designed to be used with a `flask <https://flask.palletsprojects.com/>`_ application. It can be used like:
.. code-block:: python
@scitokens_protect.protect(audience="https://demo.scitokens.org", scope="read:/secret", issuer="https://demo.scitokens.org")
def Secret(token: SciToken):
# ... token is now available.
The possible arguments are:
- ``audience`` (str or list): Audience expected in the client token
- ``scope`` (str): Scope required to access the function
- ``issuer`` (str): The issuer to require of the client token
The protected function can optionally take an argument ``token``, which is the parsed SciToken object.
Configuration
-------------
An optional configuration file can be provided that will alter the behavior of
the SciTokens library. Configuration options include:
================== ========================================================================================
Key Description
================== ========================================================================================
log_level The log level for which to use. Options include: CRITICAL, ERROR, WARNING, INFO, DEBUG.
Default: WARNING
log_file The full path to the file to log.
Default: None
cache_lifetime The minimum lifetime (in seconds) of keys in the keycache.
Default: 3600 seconds
cache_location The directory to store the KeyCache, used to store public keys across executions.
Default: $HOME/.cache/scitokens
================== ========================================================================================
The configuration file is in the ini format, and will look similar to:
::
[scitokens]
log_level = DEBUG
cache_lifetime = 60
You may set the configuration by passing a file name to ``scitokens.set_config`` function:
::
>> import scitokens
>> scitokens.set_config("/etc/scitokens/scitokens.ini")
Project Status
==============
|pypi| |build| |coverage| |quality| |docs|
.. |pypi| image:: https://badge.fury.io/py/scitokens.svg
:target: https://pypi.org/project/scitokens/
.. |downloads| image:: https://img.shields.io/pypi/dd/scitokens
:target: https://pypi.org/project/scitokens
:alt: Downloads per month
.. |license| image:: https://img.shields.io/github/license/scitokens/scitokens
:target: https://choosealicense.com/licenses/apache-2.0/
:alt: License information
.. |build| image:: https://img.shields.io/github/workflow/status/scitokens/scitokens/Python%20package
:target: https://github.com/scitokens/scitokens/actions/workflows/python-package.yml
:alt: Build pipeline status
.. |coverage| image:: https://app.codacy.com/project/badge/Coverage/753108a9f8ab450d8f5598e1b639ecfd
:target: https://www.codacy.com/gh/scitokens/scitokens/dashboard?utm_source=github.com&utm_medium=referral&utm_content=scitokens/scitokens&utm_campaign=Badge_Coverage
:alt: Code coverage
.. |quality| image:: https://app.codacy.com/project/badge/Grade/753108a9f8ab450d8f5598e1b639ecfd
:target: https://www.codacy.com/gh/scitokens/scitokens/dashboard?utm_source=github.com&utm_medium=referral&utm_content=scitokens/scitokens&utm_campaign=Badge_Grade
:alt: Code Quality
.. |docs| image:: https://readthedocs.org/projects/scitokens/badge/?version=latest
:target: https://scitokens.readthedocs.io/en/latest/?badge=latest
:alt: Documentation Status
Raw data
{
"_id": null,
"home_page": "https://scitokens.org",
"name": "scitokens",
"maintainer": "",
"docs_url": null,
"requires_python": ">=3.5",
"maintainer_email": "",
"keywords": "",
"author": "Brian Bockelman",
"author_email": "team@scitokens.org",
"download_url": "https://files.pythonhosted.org/packages/dc/13/13372ff4f4b6335819d2592b7f10ff1e56513261558b2a4162f6d6cba971/scitokens-1.8.1.tar.gz",
"platform": null,
"description": "SciTokens Library\n=================\n\n|pypi| |downloads| |license|\n\nThis library aims to be a reference implementation of the SciTokens'\nJSON Web Token (JWT) token format.\n\nSciTokens is built on top of the\n`PyJWT <https://github.com/jpadilla/pyjwt>`__ and\n`cryptography <https://cryptography.io/en/latest/>`__ libraries. We aim\nto provide a safe, high-level interface for token manipulation, avoiding\ncommon pitfalls of using the underling libraries directly.\n\n*NOTE*: SciTokens (the format and this library) is currently being\ndesigned; this README describes how we would like it to work, not\nnecessarily current functionality. Particularly, we do not foresee the\nchained tokens described here as part of the first release's\nfunctionality. The ideas behind the separate ``Validator`` in this\nlibrary is taken from\n`libmacaroons <https://github.com/rescrv/libmacaroons>`__.\n\nGenerating Tokens\n-----------------\n\nUsage revolves around the ``SciToken`` object. This can be generated\ndirectly:\n\n::\n\n >>> import scitokens\n >>> token = scitokens.SciToken() # Create token and generate a new private key\n >>> token2 = scitokens.SciToken(key=private_key) # Create token using existing key\n\nwhere ``key`` is a private key object (more later on generating private\nkeys). Direct generation using a private key will most often be done to\ndo a *base token*. SciTokens can be chained, meaning one token can be\nappended to another:\n\n::\n\n >>> token = scitokens.SciToken(parent=parent_token)\n\nThe generated object, ``token``, will default to having all the\nauthoriations of the parent token - but is mutable and can add further\nrestrictions.\n\nTokens contain zero or more claims, which are facts about the token that\ntypically indicate some sort of authorization the bearer of the token\nhas. A token has a list of key-value pairs; each token can only have a\nsingle value per key, but multiple values per key can occur in a token\nchain.\n\nTo set a claim, one can use dictionary-like setter:\n\n::\n\n >>> token['claim1'] = 'value2'\n\nThe value of each claim should be a Python object that can be serialized\nto JSON.\n\nToken Serialization\n-------------------\n\nParent tokens are typically generated by a separate server and sent as a\nresponse to a successful authentication or authorization request.\nSciTokens are built on top of JSON Web Tokens (JWT), which define a\nuseful base64-encoded serialization format. A serialized token may look\nsomething like this:\n\n::\n\n eyJhbGciOiJFUzI1NiIsImN3ayI6eyJ5IjoiazRlM1FFeDVjdGJsWmNrVkhINlkzSFZoTzFadUxVVWNZQW5ON0xkREV3YyIsIngiOiI4TkU2ZEE2T1g4NHBybHZEaDZUX3kwcWJOYmc5a2xWc2pYQnJnSkw5aElBIiwiY3J2IjoiUC0yNTYiLCJrdHkiOiJFQyJ9LCJ0eXAiOiJKV1QiLCJ4NXUiOiJodHRwczovL3ZvLmV4YW1wbGUuY29tL0pXUyJ9.eyJyZWFkIjoiL2xpZ28ifQ.uXVzbcOBCK4S4W89HzlWNmnE9ZcpuRHKTrTXYv8LZL9cDy3Injf97xNPm756fKcYwBO5KykYngFrUSGa4owglA.eyJjcnYiOiAiUC0yNTYiLCAia3R5IjogIkVDIiwgImQiOiAieWVUTTdsVXk5bGJEX2hnLVVjaGp0aXZFWHZxSWxoelJQVEVaZDBaNFBpOCJ9\n\nThis is actually 4 separate base64-encoded strings, separated by the\n``.`` character. The four pieces are:\n\n- A *header*, implementing the JSON Web Key standard, specifying the\n cryptographic properties of the token.\n- A *payload*, specifying the claims (key-value pairs) encoded by the\n token and asserted by the VO.\n- A *signature* of the header and payload, ensuring authenticity of the\n payload.\n- A *key*, utilized to sign any derived tokens. The key is an optional\n part of the token format, but may be required by some remote\n services.\n\nGiven a serialized token, the ``scitokens`` library can deserialize it:\n\n::\n\n >>> token = scitokens.SciToken.deserialize(token_serialized_bytes)\n\nAs part of the deserialization, the ``scitokens`` library will throw an\nexception if token verification failed.\n\nThe existing token can be serialized with the ``serialize`` method:\n\n::\n\n >>> token_serialized_bytes = token.serialize()\n\nValidating Tokens\n-----------------\n\nIn SciTokens, we try to distinguish between *validating* and *verifying*\ntokens. Here, verification refers to determining the integrity and\nauthenticity of the token: can we validate the token came from a known\nsource without tampering? Can we validate the chain of trust? Validation\nis determining whether the claims of the token are satisfied in a given\ncontext.\n\nFor example, if a token contains the claims\n``{vo: ligo, op: read, path: /ligo}``, we would first verify that the\ntoken is correctly signed by a known public key associated with LIGO.\nWhen presented to a storage system along with an HTTP request, the\nstorage system would validate the token authorizes the corresponding\nrequest (is it a GET request? Is it for a sub-path of /ligo?).\n\nWithin the ``scitokens`` module, validation is done by the ``Validator``\nobject:\n\n::\n\n >>> val = scitokens.Validator()\n\nThis object can be reused for multiple validations. All SciToken claims\nmust be validated. There are no \"optional\" claim attributes or values.\n\nTo validate a specific claim, provide a callback function to the\n``Validator`` object:\n\n::\n\n >>> def validate_op(value):\n ... return value == True\n >>> val.add_validator(\"op\", validate_op)\n\nOnce all the known validator callbacks have been registered, use the\n``validate`` method with a token:\n\n::\n\n >>> val.validate(token)\n\nThis will throw a ``ValidationException`` if the token could not be\nvalidated.\n\nEnforcing SciTokens Logic\n-------------------------\nFor most users of SciTokens, determining that a token is valid is insufficient.\nRather, most will be asking \"does this token allow the current resource\nrequest?\" The valid token must be compared to some action the user is\nattempting to take.\n\nTo assist in the authorization enforcement, the SciTokens library provides\nthe ``Enforcer`` class.\n\nAn unique Enforcer object is needed for each thread and issuer:\n\n::\n\n >>> enf = scitokens.Enforcer(\"https://scitokens.org/dteam\")\n\nThis object will accept tokens targetted to any audience; a more typical\nuse case will look like the following:\n\n::\n\n >>> enf = scitokens.Enforcer(\"https://scitokens.org/dteam\",\n audience=\"https://example.com\")\n\nThis second enforcer would not accept tokens that are intended for\nhttps://google.com.\n\nThe enforcer can then test authorization logic against a valid token:\n\n::\n\n >>> token = \"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtp...\"\n >>> stoken = scitokens.SciToken.deserialize(token)\n >>> enf.generate_acls(stoken)\n [(u'write', u'/store/user/bbockelm'), (u'read', u'/store')]\n >>> enf.test(stoken, \"read\", \"/store/foo\")\n True\n >>> enf.test(stoken, \"write\", \"/store/foo\")\n False\n >>> enf.test(stoken, \"write\", \"/store/user/foo\")\n False\n >>> enf.test(stoken, \"write\", \"/store/user/bbockelm/foo\")\n True\n\nThe ``test`` method uses the SciTokens built-in path parsing to validate the\nauthorization. The ``generate_acls`` method allows the caller to cache\nthe ACL information from the token.\n\nCreating Sample Tokens\n----------------------\n\nTypically, an access token is generated during an OAuth2 workflow to facilitate \nauthentication and authorization. However, for testing and experimentation purposes, \n`the demo token generator <https://demo.scitokens.org/>`__ provides users with the\nability to create sample tokens with customized payload:\n\n::\n \n >>> payload = {\"sub\": \"<email adress>\", \"scope\": \"read:/protected\"}\n >>> token = scitokens.utils.demo.token(payload)\n\nThe ``token`` method makes a request to the generator to create a serialized token \nfor the specified payload. Users can also retrieve a parsed token by calling the \n``parsed_token`` method, which returns a SciToken object corresponding to the \ntoken. The object contains the decoded token data, including the claims and signature. \n\nDecorator\n-------------\n\nThis protect decorator is designed to be used with a `flask <https://flask.palletsprojects.com/>`_ application. It can be used like:\n\n.. code-block:: python\n\n @scitokens_protect.protect(audience=\"https://demo.scitokens.org\", scope=\"read:/secret\", issuer=\"https://demo.scitokens.org\")\n def Secret(token: SciToken):\n # ... token is now available.\n\nThe possible arguments are:\n\n- ``audience`` (str or list): Audience expected in the client token\n- ``scope`` (str): Scope required to access the function\n- ``issuer`` (str): The issuer to require of the client token\n\nThe protected function can optionally take an argument ``token``, which is the parsed SciToken object.\n\nConfiguration\n-------------\n\nAn optional configuration file can be provided that will alter the behavior of \nthe SciTokens library. Configuration options include:\n\n================== ========================================================================================\nKey Description\n================== ========================================================================================\nlog_level The log level for which to use. Options include: CRITICAL, ERROR, WARNING, INFO, DEBUG.\n Default: WARNING\nlog_file The full path to the file to log.\n Default: None\ncache_lifetime The minimum lifetime (in seconds) of keys in the keycache.\n Default: 3600 seconds\ncache_location The directory to store the KeyCache, used to store public keys across executions.\n Default: $HOME/.cache/scitokens\n================== ========================================================================================\n\nThe configuration file is in the ini format, and will look similar to:\n\n::\n\n [scitokens]\n log_level = DEBUG\n cache_lifetime = 60\n\nYou may set the configuration by passing a file name to ``scitokens.set_config`` function:\n\n::\n \n >> import scitokens\n >> scitokens.set_config(\"/etc/scitokens/scitokens.ini\")\n \n\n\nProject Status\n==============\n\n|pypi| |build| |coverage| |quality| |docs|\n\n.. |pypi| image:: https://badge.fury.io/py/scitokens.svg\n :target: https://pypi.org/project/scitokens/\n\n.. |downloads| image:: https://img.shields.io/pypi/dd/scitokens\n :target: https://pypi.org/project/scitokens\n :alt: Downloads per month\n\n.. |license| image:: https://img.shields.io/github/license/scitokens/scitokens\n :target: https://choosealicense.com/licenses/apache-2.0/\n :alt: License information\n\n.. |build| image:: https://img.shields.io/github/workflow/status/scitokens/scitokens/Python%20package\n :target: https://github.com/scitokens/scitokens/actions/workflows/python-package.yml\n :alt: Build pipeline status\n\n.. |coverage| image:: https://app.codacy.com/project/badge/Coverage/753108a9f8ab450d8f5598e1b639ecfd \n :target: https://www.codacy.com/gh/scitokens/scitokens/dashboard?utm_source=github.com&utm_medium=referral&utm_content=scitokens/scitokens&utm_campaign=Badge_Coverage\n :alt: Code coverage\n\n.. |quality| image:: https://app.codacy.com/project/badge/Grade/753108a9f8ab450d8f5598e1b639ecfd \n :target: https://www.codacy.com/gh/scitokens/scitokens/dashboard?utm_source=github.com&utm_medium=referral&utm_content=scitokens/scitokens&utm_campaign=Badge_Grade\n :alt: Code Quality\n\n.. |docs| image:: https://readthedocs.org/projects/scitokens/badge/?version=latest\n :target: https://scitokens.readthedocs.io/en/latest/?badge=latest\n :alt: Documentation Status\n",
"bugtrack_url": null,
"license": "Apache-2.0",
"summary": "SciToken reference implementation library",
"version": "1.8.1",
"project_urls": {
"Documentation": "https://scitokens.readthedocs.io/",
"Homepage": "https://scitokens.org",
"Issue Tracker": "https://github.com/scitokens/scitokens/issues",
"Source Code": "https://github.com/scitokens/scitokens"
},
"split_keywords": [],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "4017916811af05e98d86b347f3a9d9c2d490ef9e5c75dc5980842bf946a70ba3",
"md5": "4907d84e30aa66d96ab25faa282614c2",
"sha256": "a5455d85969cd7c7b341ed8691ea89e0e446bd414a734f8443752081b049b46c"
},
"downloads": -1,
"filename": "scitokens-1.8.1-py3-none-any.whl",
"has_sig": false,
"md5_digest": "4907d84e30aa66d96ab25faa282614c2",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.5",
"size": 31190,
"upload_time": "2023-08-09T14:17:26",
"upload_time_iso_8601": "2023-08-09T14:17:26.835037Z",
"url": "https://files.pythonhosted.org/packages/40/17/916811af05e98d86b347f3a9d9c2d490ef9e5c75dc5980842bf946a70ba3/scitokens-1.8.1-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "dc1313372ff4f4b6335819d2592b7f10ff1e56513261558b2a4162f6d6cba971",
"md5": "db360d658e893b8bfd9625fbc34ccfa9",
"sha256": "f255383d9c7402b3fcd20d5ed26a6b407b4be8bec6f282d0af29b6275382b54d"
},
"downloads": -1,
"filename": "scitokens-1.8.1.tar.gz",
"has_sig": false,
"md5_digest": "db360d658e893b8bfd9625fbc34ccfa9",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.5",
"size": 48564,
"upload_time": "2023-08-09T14:17:27",
"upload_time_iso_8601": "2023-08-09T14:17:27.934525Z",
"url": "https://files.pythonhosted.org/packages/dc/13/13372ff4f4b6335819d2592b7f10ff1e56513261558b2a4162f6d6cba971/scitokens-1.8.1.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2023-08-09 14:17:27",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "scitokens",
"github_project": "scitokens",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"requirements": [],
"lcname": "scitokens"
}