comparison doc/rest.txt @ 5878:1b57d8f3eb97

Add rudimentery experiment JSON Web Token (jwt) support issue2551061: Add rudimentary experimental support for JSON Web Tokens to allow delegation of limited access rights to third parties. See doc/rest.txt for details and intent.
author John Rouillard <rouilj@ieee.org>
date Fri, 27 Sep 2019 20:38:31 -0400
parents 04deafac71ab
children 94a7669677ae
comparison
equal deleted inserted replaced
5877:08b241c9fea4 5878:1b57d8f3eb97
1235 However the templating system can access the hyperdb directly which 1235 However the templating system can access the hyperdb directly which
1236 allows filtering to happen with admin privs escaping the standard 1236 allows filtering to happen with admin privs escaping the standard
1237 permissions scheme. For example access to a user's roles should be 1237 permissions scheme. For example access to a user's roles should be
1238 limited to the user (read only) and an admin. If you have customized 1238 limited to the user (read only) and an admin. If you have customized
1239 your schema to implement `Restricting the list of 1239 your schema to implement `Restricting the list of
1240 users that are assignable to a task <customizing.html#restricting-the-list-of-users-that-are-assignable-to-a-task>`__ so that only users with a 1240 users that are assignable to a task <customizing.html#restricting-the-list-of-users-that-are-assignable-to-a-task>`__
1241 so that only users with a
1241 Developer role are allowed to be assigned to an issue, a rest end 1242 Developer role are allowed to be assigned to an issue, a rest end
1242 point must be added to provide a view that exposes users with this 1243 point must be added to provide a view that exposes users with this
1243 permission. 1244 permission.
1244 1245
1245 Using the normal ``/data/user?roles=Developer`` will return all the 1246 Using the normal ``/data/user?roles=Developer`` will return all the
1331 1332
1332 assuming user 4 is the only user with the Developer role. Note that 1333 assuming user 4 is the only user with the Developer role. Note that
1333 the url passes the `roles=User` filter option which is silently 1334 the url passes the `roles=User` filter option which is silently
1334 ignored. 1335 ignored.
1335 1336
1337 Changing Access Roles with JSON Web Tokens
1338 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1339
1340 As discussed above Roundup's schema is the access control mechanism.
1341 However you may want to integrate a third party system with roundup.
1342 E.G. suppose you use a time tracking service that takes an issue id
1343 and keeps a running count of how much time was spent on it. Then with
1344 a single button push it can add the recorded time to the roundup
1345 issue.
1346
1347 You probably don't want to give this third party service your roundup
1348 username and credentials. Especially if your roundup instance is under
1349 your company's single sign on infrastructure.
1350
1351 So what we need is a way for this third part service to impersonate
1352 you and have access to create a roundup timelog entry (see
1353 `<customizing.html#adding-a-time-log-to-your-issues>`__. Then add it
1354 to the associated issue. This should happen without sharing passwords
1355 and without the third party service to see the issue (except the
1356 ``times`` property), user, or other information in the tracker.
1357
1358 Enter the use of a JSON web token. Roundup has rudimentary ability to
1359 manage JWTs and use them for authentication and authorization.
1360
1361 There are 5 steps to set this up:
1362
1363 1. install pyjwt library using pip or pip3. If roundup can't find the
1364 jwt module you will see the error ``Support for jwt disabled.``
1365 2. create a new role that allows Create access to timelog and edit
1366 access to an issues' ``times`` property.
1367 3. add support for issuing (and validating) jwts to the rest interface.
1368 This uses the `Adding new rest endpoints`_ mechanism.
1369 4. configure roundup's config.ini [web] jwt_secret with at least 32
1370 random characters of data. (You will get a message
1371 ``Support for jwt disabled by admin.`` if it's not long enough.)
1372 5. add an auditor to make sure that users with this role are appending
1373 timelog links to the `times` property of the issue.
1374
1375 Create role
1376 """""""""""
1377
1378 Adding this snippet of code to the tracker's ``schema.py`` should create a role with the
1379 proper authorization::
1380
1381 db.security.addRole(name="User:timelog", description="allow a user to create and append timelogs")
1382 perm = db.security.addPermission(name='Create', klass='timelog',
1383 description="Allow timelog creation", props_only=False)
1384 db.security.addPermissionToRole("User:timelog", perm)
1385 perm = db.security.addPermission(name='Edit', klass='issue',
1386 properties=('id', 'times'),
1387 description="Allow editing timelog for issue", props_only=False)
1388 db.security.addPermissionToRole("User:timelog", perm)
1389
1390 Then role is named to work with the jwt issue rest call. Starting the role
1391 name with ``User:`` allows the jwt issue code to create a token with
1392 this role if the user requesting the role has the User role.
1393
1394 Create rest endpoints
1395 """""""""""""""""""""
1396
1397 Here is code to add to your tracker's ``interfaces.py`` (note code is
1398 python3)::
1399
1400 from roundup.rest import Routing, RestfulInstance, _data_decorator
1401
1402 class RestfulInstance(object):
1403 @Routing.route("/jwt/issue", 'POST')
1404 @_data_decorator
1405 def generate_jwt(self, input):
1406 import jwt
1407 import datetime
1408 from roundup.anypy.strings import b2s
1409
1410 # require basic auth to generate a token
1411 # At some point we can support a refresh token.
1412 # maybe a jwt with the "refresh": True claim generated
1413 # using: "refresh": True in the json request payload.
1414
1415 denialmsg='Token creation requires login with basic auth.'
1416 if 'HTTP_AUTHORIZATION' in self.client.env:
1417 try:
1418 auth = self.client.env['HTTP_AUTHORIZATION']
1419 scheme, challenge = auth.split(' ', 1)
1420 except (ValueError, AttributeError):
1421 # bad format for header
1422 raise Unauthorised(denialmsg)
1423 if scheme.lower() != 'basic':
1424 raise Unauthorised(denialmsg)
1425 else:
1426 raise Unauthorised(denialmsg)
1427
1428 # If we reach this point we have validated that the user has
1429 # logged in with a password using basic auth.
1430 all_roles = list(self.db.security.role.items())
1431 rolenames = []
1432 for role in all_roles:
1433 rolenames.append(role[0])
1434
1435 user_roles = list(self.db.user.get_roles(self.db.getuid()))
1436
1437 claim= { 'sub': self.db.getuid(),
1438 'iss': self.db.config.TRACKER_WEB,
1439 'aud': self.db.config.TRACKER_WEB,
1440 'iat': datetime.datetime.utcnow(),
1441 }
1442
1443 lifetime = 0
1444 if 'lifetime' in input:
1445 if input['lifetime'].value != 'unlimited':
1446 try:
1447 lifetime = datetime.timedelta(seconds=int(input['lifetime'].value))
1448 except ValueError:
1449 raise UsageError("Value 'lifetime' must be 'unlimited' or an integer to specify" +
1450 " lifetime in seconds. Got %s."%input['lifetime'].value)
1451 else:
1452 lifetime = datetime.timedelta(seconds=86400) # 1 day by default
1453
1454 if lifetime: # if lifetime = 0 make unlimited by omitting exp claim
1455 claim['exp'] = datetime.datetime.utcnow() + lifetime
1456
1457 newroles = []
1458 if 'roles' in input:
1459 for role in input['roles'].value:
1460 if role not in rolenames:
1461 raise UsageError("Role %s is not valid."%role)
1462 if role in user_roles:
1463 newroles.append(role)
1464 continue
1465 parentrole = role.split(':', 1)[0]
1466 if parentrole in user_roles:
1467 newroles.append(role)
1468 continue
1469
1470 raise UsageError("Role %s is not permitted."%role)
1471
1472 claim['roles'] = newroles
1473 else:
1474 claim['roles'] = user_roles
1475 secret = self.db.config.WEB_JWT_SECRET
1476 myjwt = jwt.encode(claim, secret, algorithm='HS256')
1477
1478 result = {"jwt": b2s(myjwt),
1479 }
1480
1481 return 200, result
1482
1483 @Routing.route("/jwt/validate", 'GET')
1484 @_data_decorator
1485 def validate_jwt(self,input):
1486 import jwt
1487 if not 'jwt' in input:
1488 raise UsageError("jwt key must be specified")
1489
1490 myjwt = input['jwt'].value
1491
1492 secret = self.db.config.WEB_JWT_SECRET
1493 try:
1494 result = jwt.decode(myjwt, secret,
1495 algorithms=['HS256'],
1496 audience=self.db.config.TRACKER_WEB,
1497 issuer=self.db.config.TRACKER_WEB,
1498 )
1499 except jwt.exceptions.InvalidTokenError as err:
1500 return 401, str(err)
1501
1502 return 200, result
1503
1504 **Note this is sample code. Use at your own risk.** It breaks a few
1505 rules about jwts (e.g. it allows you to make unlimited lifetime
1506 jwts). If you subscribe to the concept of jwt refresh tokens, this code
1507 will have to be changed as it will only generate jwts with
1508 username/password authentication.
1509
1510 Currently use of jwts an experiment. If this appeals to you consider
1511 providing patches to existing code to:
1512
1513 1. record all jwts created by a user
1514 2. using the record to allow jwts to be revoked and ignored by the
1515 roundup core
1516 3. provide a UI page for managing/revoking jwts
1517 4. provide a rest api for revoking jwts
1518
1519 These end points can be used like::
1520
1521 curl -u demo -s -X POST -H "Referer: https://.../demo/" \
1522 -H "X-requested-with: rest" \
1523 -H "Content-Type: application/json" \
1524 --data '{"lifetime": "3600", "roles": [ "user:timelog" ] }' \
1525 https://.../demo/rest/jwt/issue
1526
1527 (note roles is a json array/list of strings not a string) to get::
1528
1529 {
1530 "data": {
1531 "jwt": "eyJ0eXAiOiJK......XxMDb-Q3oCnMpyhxPXMAk"
1532 }
1533 }
1534
1535 The jwt is shortened in the example since it's large. You can validate
1536 a jwt to see if it's still valid using::
1537
1538
1539 curl -s -H "Referer: https://.../demo/" \
1540 -H "X-requested-with: rest" \
1541 https://rouilj.dynamic-dns.net/demo/rest/jwt/validate?jwt=eyJ0eXAiOiJK......XxMDb-Q3oCnMpyhxPXMAk
1542
1543 (note no login is required) which returns::
1544
1545 {
1546 "data": {
1547 "user": "3",
1548 "roles": [
1549 "user:timelog"
1550 ],
1551 "iss": "https://.../demo/",
1552 "aud": "https://.../demo/",
1553 "iat": 1569542404,
1554 "exp": 1569546004
1555 }
1556 }
1557
1558 Final steps
1559 ^^^^^^^^^^^
1560
1561 See the `upgrading documentation`__ on how to regenerate an updated copy of
1562 config.ini using roundup-admin. Then set the ``jwt_secret`` to at
1563 least 32 characters (more is better up to 512 bits).
1564
1565 Writing an auditor that uses "db.user.get_roles" to see if the user
1566 making the change has the ``user:timelog`` role, and then comparing
1567 the original ``times`` list to the new list to verify that it is being
1568 added to and not changed otherwise is left as an exercise for the
1569 reader. (If you develop one, please contribute via the tracker:
1570 https://issues.roundup-tracker.org/.)
1571
1572 Lastly you can create a JWT using the end point above and make a rest
1573 call to create a new timelog entry and another call to update the
1574 issues times property. If you have other ideas on how jwts can be
1575 used, please share on the roundup mailing lists. See:
1576 https://sourceforge.net/p/roundup/mailman/ for directions on
1577 subscribing and for archives of the lists.
1578
1336 1579
1337 Creating Custom Rate Limits 1580 Creating Custom Rate Limits
1338 =========================== 1581 ===========================
1339 1582
1340 You can replace the default rate limiter that is configured using 1583 You can replace the default rate limiter that is configured using
1376 function. This new function uses values for the number of calls 1619 function. This new function uses values for the number of calls
1377 and period that are specific to a user. If either is set to 0, 1620 and period that are specific to a user. If either is set to 0,
1378 the defaults from ``config.ini`` file are used. 1621 the defaults from ``config.ini`` file are used.
1379 1622
1380 Test Examples 1623 Test Examples
1381 ============= 1624 ^^^^^^^^^^^^^
1382 1625
1383 Rate limit tests: 1626 Rate limit tests:
1384 1627
1385 seq 1 300 | xargs -P 20 -n 1 curl --head -si \ 1628 seq 1 300 | xargs -P 20 -n 1 curl --head -si \
1386 https://.../rest/data/status/new \# | grep Remaining 1629 https://.../rest/data/status/new \# | grep Remaining

Roundup Issue Tracker: http://roundup-tracker.org/