Skip to content

Commit 5a759cf

Browse files
authored
Merge pull request #5397 from TaykYoku/integration_oauth_p01_docs
[8.0] add dirac-login user guide, code docs
2 parents d580658 + 04e39dd commit 5a759cf

File tree

9 files changed

+446
-223
lines changed

9 files changed

+446
-223
lines changed

docs/source/AdministratorGuide/UserManagement/index.rst

Lines changed: 8 additions & 9 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?
@@ -40,12 +39,12 @@ However, having these names the same can avoid confusions at the expense of havi
4039
Consider the registration process
4140
---------------------------------
4241

43-
User management has been provided by the Registry section of the Configuration System. To manage it you can use:
42+
User management is handled within 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 <admin_registry_cmd>` for 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 is 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: 32 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,33 @@ 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+
.. versionadded:: 8.0
58+
added the possibility to generate proxy with new `dirac-login` command, use *--help* switch for more information. E.g.: dirac-login <user_group>
59+
60+
Token authorization
61+
-------------------
62+
63+
Starting with the 8.0 version of DIRAC, it is possible to authorize users through third party Identity Providers (IdP),
64+
such as `EGI Checkin <https://www.egi.eu/services/check-in/>`_ or `WLCG IAM <https://indigo-iam.github.io/v/current/>`_.
65+
You do not need a certificate for this in a terminal, but you must be registered in one of the supported IdP. The registration process is different for each IdP.
66+
67+
Once your account is created, you will be able to register with DIRAC Authorization Server using *--use-diracas* switch of the `dirac-login` command::
68+
69+
dirac-login <user_group> --use-diracas
70+
71+
You can request to return the access token instead of a proxy using *--token* key::
72+
73+
dirac-login <user_group> --token
74+
75+
But since not all services currently support tokens, you can get a proxy if you use the *--proxy* key::
76+
77+
dirac-login <user_group> --proxy --use-diracas
78+
79+
.. note:: if you want to get a proxy after logging in to DIRAC Authorization Server you must first put it in DIRAC, see "Proxy initialization".
80+
81+
If you need to end the work session in this way to remove the received access token and related information, then use the following::
82+
83+
dirac-logout
84+
85+
This command will revoke your local access token.

src/DIRAC/FrameworkSystem/Client/ProxyManagerClient.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -653,15 +653,18 @@ def getVOMSAttributes(self, chain):
653653
"""
654654
return VOMS().getVOMSAttributes(chain)
655655

656-
def getUploadedProxyLifeTime(self, DN, group):
656+
def getUploadedProxyLifeTime(self, DN, group=None):
657657
"""Get the remaining seconds for an uploaded proxy
658658
659659
:param str DN: user DN
660660
:param str group: group
661661
662662
:return: S_OK(int)/S_ERROR()
663663
"""
664-
result = self.getDBContents({"UserDN": [DN], "UserGroup": [group]})
664+
parameters = dict(UserDN=[DN])
665+
if group:
666+
parameters["UserGroup"] = [group]
667+
result = self.getDBContents(parameters)
665668
if not result["OK"]:
666669
return result
667670
data = result["Value"]

src/DIRAC/FrameworkSystem/DB/AuthDB.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
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
"""
3-
from __future__ import absolute_import
4-
from __future__ import division
5-
from __future__ import print_function
6-
73
import json
84
import time
95
import pprint
@@ -13,16 +9,13 @@
139
from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound
1410
from sqlalchemy.ext.declarative import declarative_base
1511

16-
import authlib
1712
from authlib.jose import KeySet, JsonWebKey
1813
from authlib.common.security import generate_token
1914

2015
from DIRAC import S_OK, S_ERROR
2116
from DIRAC.Core.Base.SQLAlchemyDB import SQLAlchemyDB
2217
from DIRAC.FrameworkSystem.private.authorization.utils.Tokens import OAuth2Token
2318

24-
__RCSID__ = "$Id$"
25-
2619

2720
Model = declarative_base()
2821

src/DIRAC/FrameworkSystem/private/authorization/AuthServer.py

Lines changed: 59 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
11
""" This class provides authorization server activity. """
2-
from __future__ import absolute_import
3-
from __future__ import division
4-
from __future__ import print_function
5-
62
import re
73
import six
84
import json
@@ -44,34 +40,48 @@
4440
class AuthServer(_AuthorizationServer):
4541
"""Implementation of the :class:`authlib.oauth2.rfc6749.AuthorizationServer`.
4642
43+
This framework has been changed and simplified to be used for DIRAC purposes,
44+
namely authorization on the third party side and saving the received extended
45+
long-term access tokens on the DIRAC side with the possibility of their future
46+
use on behalf of the user without his participation.
47+
48+
The idea is that DIRAC itself is not an identity provider and relies on third-party
49+
resources such as EGI Checkin or WLCG IAM.
50+
4751
Initialize::
4852
4953
server = AuthServer()
5054
"""
5155

5256
LOCATION = None
53-
REFRESH_TOKEN_EXPIRES_IN = 24 * 3600
5457

5558
def __init__(self):
56-
self.db = AuthDB()
59+
self.db = AuthDB() # place to store session information
60+
self.log = sLog
5761
self.idps = IdProviderFactory()
58-
self.proxyCli = ProxyManagerClient()
59-
self.tokenCli = TokenManagerClient()
62+
self.proxyCli = ProxyManagerClient() # take care about proxies
63+
self.tokenCli = TokenManagerClient() # take care about tokens
64+
# The authorization server has its own settings, but they are standardized
6065
self.metadata = collectMetadata()
6166
self.metadata.validate()
62-
# args for authlib < 1.0.0: (query_client=self.query_client, save_token=None, metadata=self.metadata)
63-
# for authlib >= 1.0.0:
67+
# Initialize AuthorizationServer
6468
_AuthorizationServer.__init__(self, scopes_supported=self.metadata["scopes_supported"])
65-
# Skip authlib method save_token and send_signal
69+
# authlib requires the following methods:
70+
# The following `save_token` method is called when requesting a new access token to save it after it is generated.
71+
# Let's skip this step, because getting tokens and saving them if necessary has already taken place in `generate_token` method.
6672
self.save_token = lambda x, y: None
73+
# Framework integration can re-implement this method to support signal system.
74+
# But in this implementation, this system is not used.
6775
self.send_signal = lambda *x, **y: None
76+
# The main method that will return an access token to the user (this can be a proxy)
6877
self.generate_token = self.generateProxyOrToken
6978
# Register configured grants
70-
self.register_grant(RefreshTokenGrant)
79+
self.register_grant(RefreshTokenGrant) # Enable refreshing tokens
80+
# Enable device code flow
7181
self.register_grant(DeviceCodeGrant)
7282
self.register_endpoint(DeviceAuthorizationEndpoint)
73-
self.register_endpoint(RevocationEndpoint)
74-
self.register_grant(AuthorizationCodeGrant, [CodeChallenge(required=True)])
83+
self.register_endpoint(RevocationEndpoint) # Enable revokation tokens
84+
self.register_grant(AuthorizationCodeGrant, [CodeChallenge(required=True)]) # Enable authorization code flow
7585

7686
# pylint: disable=method-hidden
7787
def query_client(self, client_id):
@@ -86,15 +96,11 @@ def query_client(self, client_id):
8696
for cli in clients:
8797
if client_id == clients[cli]["client_id"]:
8898
gLogger.debug("Found %s client:\n" % cli, pprint.pformat(clients[cli]))
99+
# Authorization successful
89100
return Client(clients[cli])
101+
# Authorization failed, client not found
90102
return None
91103

92-
def addSession(self, session):
93-
self.db.addSession(session)
94-
95-
def getSession(self, session):
96-
self.db.getSession(session)
97-
98104
def _getScope(self, scope, param):
99105
"""Get parameter scope
100106
@@ -111,29 +117,44 @@ def _getScope(self, scope, param):
111117
def generateProxyOrToken(
112118
self, client, grant_type, user=None, scope=None, expires_in=None, include_refresh_token=True
113119
):
114-
"""Generate proxy or tokens after authorization"""
120+
"""Generate proxy or tokens after authorization
121+
122+
:param client: instance of the IdP client
123+
:param grant_type: authorization grant type (unused)
124+
:param str user: user identificator
125+
:param str scope: requested scope
126+
:param expires_in: when the token should expire (unused)
127+
:param bool include_refresh_token: to include refresh token (unused)
128+
129+
:return: dict or str -- will return tokens as dict or proxy as string
130+
"""
131+
# Read requested scopes
115132
group = self._getScope(scope, "g")
116133
lifetime = self._getScope(scope, "lifetime")
134+
# Found provider name for group
117135
provider = getIdPForGroup(group)
118136

119-
# Search DIRAC username
137+
# Search DIRAC username by user ID
120138
result = getUsernameForDN(wrapIDAsDN(user))
121139
if not result["OK"]:
122140
raise OAuth2Error(result["Message"])
123141
userName = result["Value"]
124142

143+
# User request a proxy
125144
if "proxy" in scope_to_list(scope):
126145
# Try to return user proxy if proxy scope present in the authorization request
127146
if not isDownloadablePersonalProxy():
128147
raise OAuth2Error("You can't get proxy, configuration(downloadablePersonalProxy) not allow to do that.")
129-
sLog.debug(
130-
"Try to query %s@%s proxy%s" % (user, group, ("with lifetime:%s" % lifetime) if lifetime else "")
148+
self.log.debug(
149+
"Try to query %s@%s proxy%s" % (user, group, (" with lifetime:%s" % lifetime) if lifetime else "")
131150
)
151+
# Get user DNs
132152
result = getDNForUsername(userName)
133153
if not result["OK"]:
134154
raise OAuth2Error(result["Message"])
135155
userDNs = result["Value"]
136156
err = []
157+
# Try every DN to generate a proxy
137158
for dn in userDNs:
138159
sLog.debug("Try to get proxy for %s" % dn)
139160
if lifetime:
@@ -147,24 +168,27 @@ def generateProxyOrToken(
147168
result = result["Value"].dumpAllToString()
148169
if not result["OK"]:
149170
raise OAuth2Error(result["Message"])
171+
# Proxy generated
150172
return {
151173
"proxy": result["Value"].decode() if isinstance(result["Value"], bytes) else result["Value"]
152174
}
175+
# Proxy cannot be generated or not found
153176
raise OAuth2Error("; ".join(err))
154177

178+
# User request a tokens
155179
else:
156180
# Ask TokenManager to generate new tokens for user
157181
result = self.tokenCli.getToken(userName, group)
158182
if not result["OK"]:
159183
raise OAuth2Error(result["Message"])
160184
token = result["Value"]
161-
162185
# Wrap the refresh token and register it to protect against reuse
163186
result = self.registerRefreshToken(
164187
dict(sub=user, scope=scope, provider=provider, azp=client.get_client_id()), token
165188
)
166189
if not result["OK"]:
167190
raise OAuth2Error(result["Message"])
191+
# Return tokens as dictionary
168192
return result["Value"]
169193

170194
def __signToken(self, payload):
@@ -223,7 +247,7 @@ def registerRefreshToken(self, payload, token):
223247
return S_OK(token)
224248

225249
def getIdPAuthorization(self, provider, request):
226-
"""Submit subsession and return dict with authorization url and session number
250+
"""Submit subsession to authorize with chosen provider and return dict with authorization url and session number
227251
228252
:param str provider: provider name
229253
:param object request: main session request
@@ -271,14 +295,14 @@ def parseIdPAuthorizationResponse(self, response, session):
271295
# Is ID registred?
272296
result = getUsernameForDN(credDict["DN"])
273297
if not result["OK"]:
274-
comment = "Your ID is not registred in the DIRAC: %s." % credDict["ID"]
298+
comment = "ID %s is not registred in DIRAC." % credDict["ID"]
275299
payload.update(idpObj.getUserProfile().get("Value", {}))
276300
result = self.__registerNewUser(providerName, payload)
277301

278302
if result["OK"]:
279-
comment += " Administrators have been notified about you."
303+
comment += "Administrators have been notified about you."
280304
else:
281-
comment += " Please, contact the DIRAC administrators."
305+
comment += "Please, contact the DIRAC administrators."
282306

283307
# Notify user about problem
284308
html = getHTML("unregistered user!", info=comment, theme="warning")
@@ -293,16 +317,20 @@ def parseIdPAuthorizationResponse(self, response, session):
293317
return S_OK(credDict) if result["OK"] else result
294318

295319
def create_oauth2_request(self, request, method_cls=OAuth2Request, use_json=False):
296-
sLog.debug("Create OAuth2 request", "with json" if use_json else "")
320+
"""Parse request. Rewrite authlib method."""
321+
self.log.debug("Create OAuth2 request", "with json" if use_json else "")
297322
return createOAuth2Request(request, method_cls, use_json)
298323

299324
def create_json_request(self, request):
325+
"""Parse request. Rewrite authlib method."""
300326
return self.create_oauth2_request(request, HttpRequest, True)
301327

302328
def validate_requested_scope(self, scope, state=None):
303329
"""See :func:`authlib.oauth2.rfc6749.authorization_server.validate_requested_scope`"""
304330
# We also consider parametric scope containing ":" charter
305-
extended_scope = list_to_scope([re.sub(r":.*$", ":", s) for s in scope_to_list(scope or "")])
331+
extended_scope = list_to_scope(
332+
[re.sub(r":.*$", ":", s) for s in scope_to_list((scope or "").replace("+", " "))]
333+
)
306334
super(AuthServer, self).validate_requested_scope(extended_scope, state)
307335

308336
def handle_response(self, status_code=None, payload=None, headers=None, newSession=None, delSession=None):

0 commit comments

Comments
 (0)