Skip to content

Commit a033b37

Browse files
committed
docs: add comments and code documentation about token usage
add more comments and code docs fix Auth to AuthDB add docs and comments to TokenDB Rebased to get pre-commit changes using * gh pr checkout 5397 * git rebase 67f5059 * pre-commit run --all-files * black src/DIRAC/ProductionSystem/DB/ProductionDB.py src/DIRAC/TransformationSystem/DB/TransformationDB.py * git commit --no-edit -c 75afcca * git rebase upstream/integration
1 parent 8716925 commit a033b37

File tree

10 files changed

+170
-84
lines changed

10 files changed

+170
-84
lines changed

docs/source/AdministratorGuide/UserManagement/index.rst

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,9 @@ This section provides information you need for the user management.
99
What are the components involved in that.
1010
-----------------------------------------
1111

12-
* Configuration system
13-
14-
* Registry
15-
* VOMS2CSAgent
12+
- Configuration system
13+
- Registry
14+
- VOMS2CSAgent
1615

1716

1817
What is the user in DIRAC context?
@@ -42,10 +41,10 @@ Consider the registration process
4241

4342
User management has been provided by the Registry section of the Configuration System. To manage it you can use:
4443

45-
* :ref:`dirac commands <admin_registry_cmd>` to managing Registry
46-
* configuration manager application in the Web portal (need to :ref:`install WebAppDIRAC extension <installwebappdirac>`)
47-
* modify local cfg file manually (by default it located in /opt/dirac/etc/dirac.cfg)
48-
* use the :mod:`~DIRAC.ConfigurationSystem.Agent.VOMS2CSAgent` to fetch VOMS VO users
44+
- :ref:`dirac commands <registry_cmd>` to managing Registry
45+
- configuration manager application in the Web portal (need to :ref:`install WebAppDIRAC extension <installwebappdirac>`)
46+
- modify local cfg file manually (by default it located in /opt/dirac/etc/dirac.cfg)
47+
- use the :mod:`~DIRAC.ConfigurationSystem.Agent.VOMS2CSAgent` to fetch VOMS VO users
4948

5049
In a nutshell, how to edit the configuration from the portal. First, it should be noted that to be able to do this,
5150
you must be an already registered user in a group that has the appropriate permission to edit the configuration("CSAdministrator").

docs/source/UserGuide/GettingStarted/GettingUserIdentity/index.rst

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ The user will be prompted for the password used while exporting the certificate
1919
to be used with the user's private key. Do not forget it !
2020

2121
Registration with DIRAC
22-
-------------------------
22+
-----------------------
2323

2424
Users are always working in the Grid as members of some User Community. Therefore, every user must be registered
2525
with the Community DIRAC instance. You should ask the DIRAC administrators to do that, the procedure can
@@ -30,7 +30,7 @@ determines the user rights for various Grid operations. Each DIRAC installation
3030
group to which the users are attributed when the group is not explicitly specified.
3131

3232
Proxy initialization
33-
-----------------------
33+
--------------------
3434

3535
Users authenticate with DIRAC services, and therefore with the Grid services that DIRAC expose via "proxies",
3636
which you can regard as a product of personal certificates.
@@ -53,3 +53,20 @@ If another non-default user group is needed, the command becomes::
5353
$ dirac-proxy-init -g <user_group>
5454

5555
where ``user_group`` is the desired DIRAC group name for which the user is entitled.
56+
57+
Token authorization
58+
-------------------
59+
60+
Starting with the 8.0 version of DIRAC, it is possible to authorize users through third party Identity Providers (IdP),
61+
such as EGI Checkin [https://www.egi.eu/services/check-in/] or WLCG IAM (https://indico.cern.ch/event/739896/contributions/3497694/attachments/1905332/3146590/IAM-WLCG-AuthZ-Fermilab-10092019.pdf).
62+
To do this, you do not need to have a certificate if you use a terminal, the main thing is that you must be registered in one of the supported IdP. The registration process is different for each IdP.
63+
64+
Once your account is created, you will be able to register with DIRAC using the `dirac-login` command that will return tokens that will be used to access the services::
65+
66+
dirac-login -g <user_group>
67+
68+
But since not all services currently support tokens, you can get a proxy if you use the *--proxy* key::
69+
70+
dirac-login -g <user_group> --proxy
71+
72+
Note that to get a proxy you must first put it in DIRAC, see "Proxy initialization".

src/DIRAC/Core/Tornado/Server/TornadoREST.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,10 @@ def web_users(self, count: int = 0):
6565
and we are running using executors, the methods you export cannot write
6666
back directly to the client. Please see inline comments for more details.
6767
68-
In order to pass information around and keep some states, we use instance attributes.
69-
These are initialized in the :py:meth:`.initialize` method.
70-
7168
The handler define the ``post`` and ``get`` verbs. Please refer to :py:meth:`.post` for the details.
7269
"""
7370

71+
# By default we enable all authorization grants
7472
USE_AUTHZ_GRANTS = ["SSL", "JWT", "VISITOR"]
7573
METHOD_PREFIX = "web_"
7674
LOCATION = "/"
@@ -118,15 +116,15 @@ def _getMethodName(self):
118116
@gen.coroutine
119117
def get(self, *args, **kwargs): # pylint: disable=arguments-differ
120118
"""Method to handle incoming ``GET`` requests.
121-
Note that all the arguments are already prepared in the :py:meth:`.prepare` method.
119+
Note that all the arguments are already prepared in the :py:meth:`BaseRequestHandler.prepare` method.
122120
"""
123121
retVal = yield IOLoop.current().run_in_executor(*self._prepareExecutor(args))
124122
self._finishFuture(retVal)
125123

126124
@gen.coroutine
127125
def post(self, *args, **kwargs): # pylint: disable=arguments-differ
128126
"""Method to handle incoming ``POST`` requests.
129-
Note that all the arguments are already prepared in the :py:meth:`.prepare` method.
127+
Note that all the arguments are already prepared in the :py:meth:`BaseRequestHandler.prepare` method.
130128
"""
131129
retVal = yield IOLoop.current().run_in_executor(*self._prepareExecutor(args))
132130
self._finishFuture(retVal)
@@ -135,9 +133,7 @@ def post(self, *args, **kwargs): # pylint: disable=arguments-differ
135133

136134
@staticmethod
137135
def web_echo(data):
138-
"""
139-
This method used for testing the performance of a service
140-
"""
136+
"""This method used for testing the performance of a service"""
141137
return S_OK(data)
142138

143139
auth_whoami = ["authenticated"]

src/DIRAC/Core/Tornado/Server/TornadoService.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,16 @@ def export_streamToClient(self, myDataToSend, token):
8484
with open(myFileToSend, 'r') as fd:
8585
return fd.read()
8686
87-
8887
Note that because we inherit from :py:class:`tornado.web.RequestHandler`
8988
and we are running using executors, the methods you export cannot write
9089
back directly to the client. Please see inline comments for more details.
9190
92-
In order to pass information around and keep some states, we use instance attributes.
93-
These are initialized in the :py:meth:`.initialize` method.
91+
For compatibility with the existing :py:class:`DIRAC.Core.DISET.TransferClient.TransferClient`,
92+
the handler can define a method ``export_streamToClient``. This is the method that will be called
93+
whenever ``TransferClient.receiveFile`` is called. It is the equivalent of the DISET
94+
``transfer_toClient``.
95+
Note that this is here only for compatibility, and we discourage using it for new purposes, as it is
96+
bound to disappear.
9497
9598
The handler only define the ``post`` verb. Please refer to :py:meth:`.post` for the details.
9699

src/DIRAC/Core/Tornado/Server/private/BaseRequestHandler.py

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,20 @@
3838

3939

4040
class TornadoResponse(object):
41-
"""This class registers tornadoes with arguments in the order they are called
41+
"""This class registers tornado methods with arguments in the order they are called
4242
from TornadoResponse to call them later.
4343
44-
Use::
44+
This is used in exceptional cases, in most cases it is not required,
45+
just use `return S_OK(data)` instead.
46+
47+
Usage example::
4548
4649
def web_myEndpoint(self):
47-
resp = TornadoResponse("data")
48-
resp.set_status(400)
49-
return resp
50+
# We need not only to return the result but also to set a header.
51+
resp = TornadoResponse('data')
52+
resp.set_header("Content-Type", "application/x-tar")
53+
# And for example redirect to another place
54+
return resp.redirect('https://server')
5055
"""
5156

5257
__attrs = inspect.getmembers(RequestHandler)
@@ -75,7 +80,9 @@ def __setAction(self, mName, *args, **kwargs):
7580
return self
7681

7782
def _runActions(self, reqObj):
78-
"""Calling methods in the order of their registration
83+
"""Calling methods in the order of their registration.
84+
This method is called at the end of the request, when the main work is already done in the thread.
85+
Look the `_finishFuture` method.
7986
8087
:param reqObj: RequestHandler instance
8188
"""
@@ -88,7 +95,7 @@ def _runActions(self, reqObj):
8895

8996

9097
class BaseRequestHandler(RequestHandler):
91-
"""Base class for all the Handlers.
98+
"""Base class for all the Handlers that uses HTTP tornado framework on server side.
9299
It directly inherits from :py:class:`tornado.web.RequestHandler`
93100
94101
Each HTTP request is served by a new instance of this class.
@@ -104,8 +111,8 @@ class BaseRequestHandler(RequestHandler):
104111
105112
class TornadoInstance(BaseRequestHandler):
106113
107-
# Prefix of methods names
108-
METHOD_PREFIX = "export_"
114+
# Prefix of methods names if need to use a special prefix. By default its "export_".
115+
METHOD_PREFIX = "web_"
109116
110117
@classmethod
111118
def _getServiceName(cls, request):
@@ -115,7 +122,7 @@ def _getServiceName(cls, request):
115122
116123
@classmethod
117124
def _getServiceInfo(cls, serviceName, request):
118-
''' Fill service information.
125+
''' Fill service information. By default return empty dictionary.
119126
'''
120127
return {'serviceName': serviceName,
121128
'serviceSectionPath': PathFinder.getServiceSection(serviceName),
@@ -156,12 +163,8 @@ def post(self, *args, **kwargs): # pylint: disable=arguments-differ
156163
# retVal is :py:class:`tornado.concurrent.Future`
157164
self._finishFuture(retVal)
158165
159-
For compatibility with the existing :py:class:`DIRAC.Core.DISET.TransferClient.TransferClient`,
160-
the handler can define a method ``export_streamToClient``. This is the method that will be called
161-
whenever ``TransferClient.receiveFile`` is called. It is the equivalent of the DISET
162-
``transfer_toClient``.
163-
Note that this is here only for compatibility, and we discourage using it for new purposes, as it is
164-
bound to disappear.
166+
In order to pass information around and keep some states, we use instance attributes.
167+
These are initialized in the :py:meth:`.initialize` method.
165168
"""
166169

167170
# Because we initialize at first request, we use a flag to know if it's already done
@@ -342,9 +345,8 @@ def initializeHandler(cls, serviceInfo):
342345
And it must be a class method. This method is called only one time,
343346
at the first request
344347
345-
:param dict ServiceInfoDict: infos about services, it contains
346-
'serviceName', 'serviceSectionPath',
347-
'csPaths' and 'URL'
348+
:param dict serviceInfo: infos about services, it contains information about service, e.g.:
349+
'serviceName', 'serviceSectionPath', 'csPaths' and 'URL'
348350
"""
349351
pass
350352

@@ -424,15 +426,18 @@ def _getMethodAuthProps(self):
424426
425427
:return: list
426428
"""
429+
# Cover default authorization requirements to list
427430
if self.AUTH_PROPS and not isinstance(self.AUTH_PROPS, (list, tuple)):
428431
self.AUTH_PROPS = [p.strip() for p in self.AUTH_PROPS.split(",") if p.strip()]
432+
# Use auth_< method name > as primary value of the authorization requirements
429433
return getattr(self, "auth_" + self.mehtodName, self.AUTH_PROPS)
430434

431435
def _getMethod(self):
432436
"""Get method function to call.
433437
434438
:return: function
435439
"""
440+
# Get method object using prefix and method name from request
436441
methodObj = getattr(self, "%s%s" % (self.METHOD_PREFIX, self.mehtodName), None)
437442
if not callable(methodObj):
438443
sLog.error("Invalid method", self.mehtodName)

src/DIRAC/FrameworkSystem/DB/AuthDB.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
""" Auth class is a front-end to the Auth Database
1+
""" AuthDB class is a front-end to the AuthDB MySQL Database (via SQLAlchemy)
22
"""
33
from __future__ import absolute_import
44
from __future__ import division

src/DIRAC/FrameworkSystem/DB/TokenDB.py

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
""" Auth class is a front-end to the Auth Database
1+
""" Token class is a front-end to the TokenDB Database.
2+
3+
Long-term user tokens are stored here, which can be used to obtain new tokens.
24
"""
35
from __future__ import absolute_import
46
from __future__ import division
@@ -26,19 +28,21 @@
2628

2729

2830
class Token(Model, OAuth2TokenMixin):
31+
"""This class describe token fields"""
32+
2933
__tablename__ = "Token"
3034
__table_args__ = {"mysql_engine": "InnoDB", "mysql_charset": "utf8"}
3135
# access_token too large for varchar(255)
3236
# 767 bytes is the stated prefix limitation for InnoDB tables in MySQL version 5.6
3337
# https://stackoverflow.com/questions/1827063/mysql-error-key-specification-without-a-key-length
34-
id = Column(Integer, autoincrement=True, primary_key=True)
35-
kid = Column(String(255))
36-
user_id = Column(String(255))
37-
provider = Column(String(255))
38-
expires_at = Column(Integer, nullable=False, default=0)
38+
id = Column(Integer, autoincrement=True, primary_key=True) # Unique token ID
39+
kid = Column(String(255)) # Unique secret key ID for token encryption
40+
user_id = Column(String(255)) # User identificator that registred in an identity provider, token owner
41+
provider = Column(String(255)) # Provider name registred in DIRAC
42+
expires_at = Column(Integer, nullable=False, default=0) # When the access token is expired
3943
access_token = Column(Text, nullable=False)
4044
refresh_token = Column(Text, nullable=False)
41-
rt_expires_at = Column(Integer, nullable=False, default=0)
45+
rt_expires_at = Column(Integer, nullable=False, default=0) # When the refresh token is expired
4246

4347

4448
class TokenDB(SQLAlchemyDB):
@@ -54,7 +58,10 @@ def __init__(self):
5458
self.session = scoped_session(self.sessionMaker_o)
5559

5660
def __initializeDB(self):
57-
"""Create the tables"""
61+
"""Create the tables
62+
63+
:return: S_OK()/S_ERROR()
64+
"""
5865
tablesInDB = self.inspector.get_table_names()
5966

6067
# Token
@@ -72,7 +79,7 @@ def getTokenForUserProvider(self, userID, provider):
7279
:param str userID: user ID
7380
:param str provider: provider
7481
75-
:return: S_OK(dict)/S_ERROR()
82+
:return: S_OK(OAuth2Token)/S_ERROR() -- return an OAuth2Token object, which is also a dict
7683
"""
7784
session = self.session()
7885
try:
@@ -88,34 +95,40 @@ def getTokenForUserProvider(self, userID, provider):
8895
return self.__result(session, S_OK(OAuth2Token(self.__rowToDict(token)) if token else None))
8996

9097
def updateToken(self, token, userID, provider, rt_expired_in):
91-
"""Update tokens
98+
"""Update tokens for user and identity provider
9299
93100
:param dict token: token info
94-
:param str userID: user ID
95-
:param str provider: provider
101+
:param str userID: user ID that comes from identity provider
102+
:param str provider: provider name
96103
:param int rt_expired_in: refresh token lifetime
97104
98-
:return: S_OK(list)/S_ERROR()
105+
:return: S_OK(list)/S_ERROR() -- return old tokens that should be revoked.
99106
"""
107+
# Prepare a token to write to the database
100108
token["user_id"] = userID
101109
token["provider"] = provider
110+
# If the token expiration date is not specified, we will try to determine it
102111
if not token.get("rt_expires_at"):
103112
try:
113+
# This value can be contained in the token itself if it is a JWT
104114
token["rt_expires_at"] = int(
105115
jwt.decode(token["refresh_token"], options=dict(verify_signature=False, verify_aud=False))["exp"]
106116
)
107117
except Exception as e:
108118
self.log.debug("Cannot get refresh token expires time: %s" % repr(e))
109-
119+
# Otherwise, we set this value
110120
token["rt_expires_at"] = int(token.get("rt_expires_at", rt_expired_in + int(time.time())))
121+
# We ignore expired tokens
111122
if token["rt_expires_at"] < time.time():
112123
return S_ERROR("Cannot store expired refresh token.")
113124

114125
attrts = dict((k, v) for k, v in dict(token).items() if k in list(Token.__dict__.keys()))
115126
self.log.debug("Store token:", pprint.pformat(attrts))
116127
session = self.session()
117128
try:
129+
# Remove expired tokens
118130
session.query(Token).filter(Token.expires_at < time.time()).delete()
131+
# When we update existing tokens, the old tokens should be revoked
119132
oldTokens = session.query(Token).filter(Token.user_id == userID).filter(Token.provider == provider).all()
120133
session.add(Token(**attrts))
121134
session.query(Token).filter(Token.user_id == userID).filter(Token.provider == provider).filter(
@@ -128,12 +141,12 @@ def updateToken(self, token, userID, provider, rt_expired_in):
128141
return self.__result(session, S_OK([self.__rowToDict(t) for t in oldTokens] if oldTokens else []))
129142

130143
def removeToken(self, access_token=None, refresh_token=None, user_id=None):
131-
"""Remove token
144+
"""Remove token from DB
132145
133146
:param str access_token: access token
134147
:param str refresh_token: refresh token
135148
136-
:return: S_OK(object)/S_ERROR()
149+
:return: S_OK(str)/S_ERROR()
137150
"""
138151
session = self.session()
139152
try:
@@ -148,6 +161,12 @@ def removeToken(self, access_token=None, refresh_token=None, user_id=None):
148161
return self.__result(session, S_OK("Token successfully removed"))
149162

150163
def getTokensByUserID(self, userID):
164+
"""Return tokens for user ID
165+
166+
:param str userID: user ID that return identity provider
167+
168+
:return: S_OK(list)/S_ERROR() -- tokens as OAuth2Token objects
169+
"""
151170
session = self.session()
152171
try:
153172
tokens = session.query(Token).filter(Token.user_id == userID).all()
@@ -158,6 +177,13 @@ def getTokensByUserID(self, userID):
158177
return self.__result(session, S_OK([OAuth2Token(self.__rowToDict(t)) for t in tokens]))
159178

160179
def __result(self, session, result=None):
180+
"""Helper method
181+
182+
:param session: session instance
183+
:param result: DIRAC result
184+
185+
:return: S_OK()/S_ERROR()
186+
"""
161187
try:
162188
if not result["OK"]:
163189
session.rollback()

0 commit comments

Comments
 (0)