Mercurial > p > roundup > code
comparison doc/rest.txt @ 5893:13f5ac918120
fix spacing on example code and typo fix.
| author | John Rouillard <rouilj@ieee.org> |
|---|---|
| date | Wed, 02 Oct 2019 20:55:15 -0400 |
| parents | afb5705d1fe5 |
| children | 45a104bb127e |
comparison
equal
deleted
inserted
replaced
| 5892:afb5705d1fe5 | 5893:13f5ac918120 |
|---|---|
| 1437 perm = db.security.addPermission(name='Create', klass='timelog', | 1437 perm = db.security.addPermission(name='Create', klass='timelog', |
| 1438 description="Allow timelog creation", props_only=False) | 1438 description="Allow timelog creation", props_only=False) |
| 1439 db.security.addPermissionToRole("User:timelog", perm) | 1439 db.security.addPermissionToRole("User:timelog", perm) |
| 1440 | 1440 |
| 1441 perm = db.security.addPermission(name='View', klass='issue', | 1441 perm = db.security.addPermission(name='View', klass='issue', |
| 1442 properties=('id', 'times'), | 1442 properties=('id', 'times'), |
| 1443 description="Allow timelog retreival for issue", | 1443 description="Allow retrieving issue etag or timelog issue", |
| 1444 props_only=False) | 1444 props_only=False) |
| 1445 db.security.addPermissionToRole("User:timelog", perm) | 1445 db.security.addPermissionToRole("User:timelog", perm) |
| 1446 | 1446 |
| 1447 perm = db.security.addPermission(name='Edit', klass='issue', | 1447 perm = db.security.addPermission(name='Edit', klass='issue', |
| 1448 properties=('id', 'times'), | 1448 properties=('id', 'times'), |
| 1449 description="Allow editing timelog for issue", props_only=False) | 1449 description="Allow editing timelog for issue", |
| 1450 props_only=False) | |
| 1450 db.security.addPermissionToRole("User:timelog", perm) | 1451 db.security.addPermissionToRole("User:timelog", perm) |
| 1451 | 1452 |
| 1452 The role is named to work with the /rest/jwt/issue rest endpoint | 1453 The role is named to work with the /rest/jwt/issue rest endpoint |
| 1453 defined below. Starting the role name with ``User:`` allows the jwt | 1454 defined below. Starting the role name with ``User:`` allows the jwt |
| 1454 issue code to create a token with this role if the user requesting the | 1455 issue code to create a token with this role if the user requesting the |
| 1455 role has the User role. | 1456 role has the User role. |
| 1456 | 1457 |
| 1457 The role *must* have access to the issue ``id`` to retrieve the etag for | 1458 The role *must* have access to the issue ``id`` to retrieve the etag for |
| 1458 the issue. The etag is passed in the ``If-Match`` HTTP header when you | 1459 the issue. The etag is passed in the ``If-Match`` HTTP header when you |
| 1459 make a call to patch or update the ``timess` property of the issue. | 1460 make a call to patch or update the ``times` property of the issue. |
| 1460 | 1461 |
| 1461 If you use a PATCH rest call with "@op=add" to append the new timelog, | 1462 If you use a PATCH rest call with "@op=add" to append the new timelog, |
| 1462 you don't need View access to the ``times`` property. If you replace the | 1463 you don't need View access to the ``times`` property. If you replace the |
| 1463 ``times`` value, you need to read the current value of ``times`` (using | 1464 ``times`` value, you need to read the current value of ``times`` (using |
| 1464 View permission), append the newly created timelog id to the (array) | 1465 View permission), append the newly created timelog id to the (array) |
| 1475 only been tested with python3):: | 1476 only been tested with python3):: |
| 1476 | 1477 |
| 1477 from roundup.rest import Routing, RestfulInstance, _data_decorator | 1478 from roundup.rest import Routing, RestfulInstance, _data_decorator |
| 1478 | 1479 |
| 1479 class RestfulInstance(object): | 1480 class RestfulInstance(object): |
| 1480 @Routing.route("/jwt/issue", 'POST') | 1481 @Routing.route("/jwt/issue", 'POST') |
| 1481 @_data_decorator | 1482 @_data_decorator |
| 1482 def generate_jwt(self, input): | 1483 def generate_jwt(self, input): |
| 1483 import jwt | 1484 import jwt |
| 1484 import datetime | 1485 import datetime |
| 1485 from roundup.anypy.strings import b2s | 1486 from roundup.anypy.strings import b2s |
| 1486 | 1487 |
| 1487 # require basic auth to generate a token | 1488 # require basic auth to generate a token |
| 1488 # At some point we can support a refresh token. | 1489 # At some point we can support a refresh token. |
| 1489 # maybe a jwt with the "refresh": True claim generated | 1490 # maybe a jwt with the "refresh": True claim generated |
| 1490 # using: "refresh": True in the json request payload. | 1491 # using: "refresh": True in the json request payload. |
| 1491 | 1492 |
| 1492 denialmsg='Token creation requires login with basic auth.' | 1493 denialmsg='Token creation requires login with basic auth.' |
| 1493 if 'HTTP_AUTHORIZATION' in self.client.env: | 1494 if 'HTTP_AUTHORIZATION' in self.client.env: |
| 1494 try: | 1495 try: |
| 1495 auth = self.client.env['HTTP_AUTHORIZATION'] | 1496 auth = self.client.env['HTTP_AUTHORIZATION'] |
| 1496 scheme, challenge = auth.split(' ', 1) | 1497 scheme, challenge = auth.split(' ', 1) |
| 1497 except (ValueError, AttributeError): | 1498 except (ValueError, AttributeError): |
| 1498 # bad format for header | 1499 # bad format for header |
| 1499 raise Unauthorised(denialmsg) | 1500 raise Unauthorised(denialmsg) |
| 1500 if scheme.lower() != 'basic': | 1501 if scheme.lower() != 'basic': |
| 1501 raise Unauthorised(denialmsg) | 1502 raise Unauthorised(denialmsg) |
| 1502 else: | 1503 else: |
| 1503 raise Unauthorised(denialmsg) | 1504 raise Unauthorised(denialmsg) |
| 1504 | 1505 |
| 1505 # If we reach this point we have validated that the user has | 1506 # If we reach this point we have validated that the user has |
| 1506 # logged in with a password using basic auth. | 1507 # logged in with a password using basic auth. |
| 1507 all_roles = list(self.db.security.role.items()) | 1508 all_roles = list(self.db.security.role.items()) |
| 1508 rolenames = [] | 1509 rolenames = [] |
| 1509 for role in all_roles: | 1510 for role in all_roles: |
| 1510 rolenames.append(role[0]) | 1511 rolenames.append(role[0]) |
| 1511 | 1512 |
| 1512 user_roles = list(self.db.user.get_roles(self.db.getuid())) | 1513 user_roles = list(self.db.user.get_roles(self.db.getuid())) |
| 1513 | 1514 |
| 1514 claim= { 'sub': self.db.getuid(), | 1515 claim= { 'sub': self.db.getuid(), |
| 1515 'iss': self.db.config.TRACKER_WEB, | 1516 'iss': self.db.config.TRACKER_WEB, |
| 1516 'aud': self.db.config.TRACKER_WEB, | 1517 'aud': self.db.config.TRACKER_WEB, |
| 1517 'iat': datetime.datetime.utcnow(), | 1518 'iat': datetime.datetime.utcnow(), |
| 1518 } | 1519 } |
| 1519 | 1520 |
| 1520 lifetime = 0 | 1521 lifetime = 0 |
| 1521 if 'lifetime' in input: | 1522 if 'lifetime' in input: |
| 1522 if input['lifetime'].value != 'unlimited': | 1523 if input['lifetime'].value != 'unlimited': |
| 1523 try: | 1524 try: |
| 1524 lifetime = datetime.timedelta(seconds=int(input['lifetime'].value)) | 1525 lifetime = datetime.timedelta(seconds=int(input['lifetime'].value)) |
| 1525 except ValueError: | 1526 except ValueError: |
| 1526 raise UsageError("Value 'lifetime' must be 'unlimited' or an integer to specify" + | 1527 raise UsageError("Value 'lifetime' must be 'unlimited' or an integer to specify" + |
| 1527 " lifetime in seconds. Got %s."%input['lifetime'].value) | 1528 " lifetime in seconds. Got %s."%input['lifetime'].value) |
| 1528 else: | 1529 else: |
| 1529 lifetime = datetime.timedelta(seconds=86400) # 1 day by default | 1530 lifetime = datetime.timedelta(seconds=86400) # 1 day by default |
| 1530 | 1531 |
| 1531 if lifetime: # if lifetime = 0 make unlimited by omitting exp claim | 1532 if lifetime: # if lifetime = 0 make unlimited by omitting exp claim |
| 1532 claim['exp'] = datetime.datetime.utcnow() + lifetime | 1533 claim['exp'] = datetime.datetime.utcnow() + lifetime |
| 1533 | 1534 |
| 1534 newroles = [] | 1535 newroles = [] |
| 1535 if 'roles' in input: | 1536 if 'roles' in input: |
| 1536 for role in input['roles'].value: | 1537 for role in input['roles'].value: |
| 1537 if role not in rolenames: | 1538 if role not in rolenames: |
| 1538 raise UsageError("Role %s is not valid."%role) | 1539 raise UsageError("Role %s is not valid."%role) |
| 1539 if role in user_roles: | 1540 if role in user_roles: |
| 1540 newroles.append(role) | 1541 newroles.append(role) |
| 1541 continue | 1542 continue |
| 1542 parentrole = role.split(':', 1)[0] | 1543 parentrole = role.split(':', 1)[0] |
| 1543 if parentrole in user_roles: | 1544 if parentrole in user_roles: |
| 1544 newroles.append(role) | 1545 newroles.append(role) |
| 1545 continue | 1546 continue |
| 1546 | 1547 |
| 1547 raise UsageError("Role %s is not permitted."%role) | 1548 raise UsageError("Role %s is not permitted."%role) |
| 1548 | 1549 |
| 1549 claim['roles'] = newroles | 1550 claim['roles'] = newroles |
| 1550 else: | 1551 else: |
| 1551 claim['roles'] = user_roles | 1552 claim['roles'] = user_roles |
| 1552 secret = self.db.config.WEB_JWT_SECRET | 1553 secret = self.db.config.WEB_JWT_SECRET |
| 1553 myjwt = jwt.encode(claim, secret, algorithm='HS256') | 1554 myjwt = jwt.encode(claim, secret, algorithm='HS256') |
| 1554 | 1555 |
| 1555 result = {"jwt": b2s(myjwt), | 1556 result = {"jwt": b2s(myjwt), |
| 1556 } | 1557 } |
| 1557 | 1558 |
| 1558 return 200, result | 1559 return 200, result |
| 1559 | 1560 |
| 1560 @Routing.route("/jwt/validate", 'GET') | 1561 @Routing.route("/jwt/validate", 'GET') |
| 1561 @_data_decorator | 1562 @_data_decorator |
| 1562 def validate_jwt(self,input): | 1563 def validate_jwt(self,input): |
| 1563 import jwt | 1564 import jwt |
| 1564 if not 'jwt' in input: | 1565 if not 'jwt' in input: |
| 1565 raise UsageError("jwt key must be specified") | 1566 raise UsageError("jwt key must be specified") |
| 1566 | 1567 |
| 1567 myjwt = input['jwt'].value | 1568 myjwt = input['jwt'].value |
| 1568 | 1569 |
| 1569 secret = self.db.config.WEB_JWT_SECRET | 1570 secret = self.db.config.WEB_JWT_SECRET |
| 1570 try: | 1571 try: |
| 1571 result = jwt.decode(myjwt, secret, | 1572 result = jwt.decode(myjwt, secret, |
| 1572 algorithms=['HS256'], | 1573 algorithms=['HS256'], |
| 1573 audience=self.db.config.TRACKER_WEB, | 1574 audience=self.db.config.TRACKER_WEB, |
| 1574 issuer=self.db.config.TRACKER_WEB, | 1575 issuer=self.db.config.TRACKER_WEB, |
| 1575 ) | 1576 ) |
| 1576 except jwt.exceptions.InvalidTokenError as err: | 1577 except jwt.exceptions.InvalidTokenError as err: |
| 1577 return 401, str(err) | 1578 return 401, str(err) |
| 1578 | 1579 |
| 1579 return 200, result | 1580 return 200, result |
| 1580 | 1581 |
| 1581 **Note this is sample code. Use at your own risk.** It breaks a few | 1582 **Note this is sample code. Use at your own risk.** It breaks a few |
| 1582 rules about jwts (e.g. it allows you to make unlimited lifetime | 1583 rules about jwts (e.g. it allows you to make unlimited lifetime |
| 1583 jwts). If you subscribe to the concept of jwt refresh tokens, this code | 1584 jwts). If you subscribe to the concept of jwt refresh tokens, this code |
| 1584 will have to be changed as it will only generate jwts with | 1585 will have to be changed as it will only generate jwts with |
