@@ -1439,14 +1439,15 @@ proper authorization::
14391439 db.security.addPermissionToRole("User:timelog", perm)
14401440
14411441 perm = db.security.addPermission(name='View', klass='issue',
1442- properties=('id', 'times'),
1443- description="Allow timelog retreival for issue",
1444- props_only=False)
1442+ properties=('id', 'times'),
1443+ description="Allow retrieving issue etag or timelog issue",
1444+ props_only=False)
14451445 db.security.addPermissionToRole("User:timelog", perm)
14461446
14471447 perm = db.security.addPermission(name='Edit', klass='issue',
14481448 properties=('id', 'times'),
1449- description="Allow editing timelog for issue", props_only=False)
1449+ description="Allow editing timelog for issue",
1450+ props_only=False)
14501451 db.security.addPermissionToRole("User:timelog", perm)
14511452
14521453The role is named to work with the /rest/jwt/issue rest endpoint
@@ -1456,7 +1457,7 @@ role has the User role.
14561457
14571458The role *must* have access to the issue ``id`` to retrieve the etag for
14581459the 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.
14601461
14611462If you use a PATCH rest call with "@op=add" to append the new timelog,
14621463you don't need View access to the ``times`` property. If you replace the
@@ -1477,106 +1478,106 @@ only been tested with python3)::
14771478 from roundup.rest import Routing, RestfulInstance, _data_decorator
14781479
14791480 class RestfulInstance(object):
1480- @Routing.route("/jwt/issue", 'POST')
1481- @_data_decorator
1482- def generate_jwt(self, input):
1483- import jwt
1484- import datetime
1485- from roundup.anypy.strings import b2s
1486-
1487- # require basic auth to generate a token
1488- # At some point we can support a refresh token.
1489- # maybe a jwt with the "refresh": True claim generated
1490- # using: "refresh": True in the json request payload.
1491-
1492- denialmsg='Token creation requires login with basic auth.'
1493- if 'HTTP_AUTHORIZATION' in self.client.env:
1494- try:
1495- auth = self.client.env['HTTP_AUTHORIZATION']
1496- scheme, challenge = auth.split(' ', 1)
1497- except (ValueError, AttributeError):
1498- # bad format for header
1499- raise Unauthorised(denialmsg)
1500- if scheme.lower() != 'basic':
1501- raise Unauthorised(denialmsg)
1502- else:
1503- raise Unauthorised(denialmsg)
1504-
1505- # If we reach this point we have validated that the user has
1506- # logged in with a password using basic auth.
1507- all_roles = list(self.db.security.role.items())
1508- rolenames = []
1509- for role in all_roles:
1510- rolenames.append(role[0])
1511-
1512- user_roles = list(self.db.user.get_roles(self.db.getuid()))
1513-
1514- claim= { 'sub': self.db.getuid(),
1515- 'iss': self.db.config.TRACKER_WEB,
1516- 'aud': self.db.config.TRACKER_WEB,
1517- 'iat': datetime.datetime.utcnow(),
1518- }
1519-
1520- lifetime = 0
1521- if 'lifetime' in input:
1522- if input['lifetime'].value != 'unlimited':
1523- try:
1524- lifetime = datetime.timedelta(seconds=int(input['lifetime'].value))
1525- except ValueError:
1526- raise UsageError("Value 'lifetime' must be 'unlimited' or an integer to specify" +
1527- " lifetime in seconds. Got %s."%input['lifetime'].value)
1528- else:
1529- lifetime = datetime.timedelta(seconds=86400) # 1 day by default
1530-
1531- if lifetime: # if lifetime = 0 make unlimited by omitting exp claim
1532- claim['exp'] = datetime.datetime.utcnow() + lifetime
1533-
1534- newroles = []
1535- if 'roles' in input:
1536- for role in input['roles'].value:
1537- if role not in rolenames:
1538- raise UsageError("Role %s is not valid."%role)
1539- if role in user_roles:
1540- newroles.append(role)
1541- continue
1542- parentrole = role.split(':', 1)[0]
1543- if parentrole in user_roles:
1544- newroles.append(role)
1545- continue
1546-
1547- raise UsageError("Role %s is not permitted."%role)
1548-
1549- claim['roles'] = newroles
1550- else:
1551- claim['roles'] = user_roles
1552- secret = self.db.config.WEB_JWT_SECRET
1553- myjwt = jwt.encode(claim, secret, algorithm='HS256')
1554-
1555- result = {"jwt": b2s(myjwt),
1556- }
1557-
1558- return 200, result
1559-
1560- @Routing.route("/jwt/validate", 'GET')
1561- @_data_decorator
1562- def validate_jwt(self,input):
1563- import jwt
1564- if not 'jwt' in input:
1565- raise UsageError("jwt key must be specified")
1566-
1567- myjwt = input['jwt'].value
1568-
1569- secret = self.db.config.WEB_JWT_SECRET
1570- try:
1571- result = jwt.decode(myjwt, secret,
1572- algorithms=['HS256'],
1573- audience=self.db.config.TRACKER_WEB,
1574- issuer=self.db.config.TRACKER_WEB,
1575- )
1576- except jwt.exceptions.InvalidTokenError as err:
1577- return 401, str(err)
1578-
1579- return 200, result
1481+ @Routing.route("/jwt/issue", 'POST')
1482+ @_data_decorator
1483+ def generate_jwt(self, input):
1484+ import jwt
1485+ import datetime
1486+ from roundup.anypy.strings import b2s
1487+
1488+ # require basic auth to generate a token
1489+ # At some point we can support a refresh token.
1490+ # maybe a jwt with the "refresh": True claim generated
1491+ # using: "refresh": True in the json request payload.
1492+
1493+ denialmsg='Token creation requires login with basic auth.'
1494+ if 'HTTP_AUTHORIZATION' in self.client.env:
1495+ try:
1496+ auth = self.client.env['HTTP_AUTHORIZATION']
1497+ scheme, challenge = auth.split(' ', 1)
1498+ except (ValueError, AttributeError):
1499+ # bad format for header
1500+ raise Unauthorised(denialmsg)
1501+ if scheme.lower() != 'basic':
1502+ raise Unauthorised(denialmsg)
1503+ else:
1504+ raise Unauthorised(denialmsg)
1505+
1506+ # If we reach this point we have validated that the user has
1507+ # logged in with a password using basic auth.
1508+ all_roles = list(self.db.security.role.items())
1509+ rolenames = []
1510+ for role in all_roles:
1511+ rolenames.append(role[0])
1512+
1513+ user_roles = list(self.db.user.get_roles(self.db.getuid()))
1514+
1515+ claim= { 'sub': self.db.getuid(),
1516+ 'iss': self.db.config.TRACKER_WEB,
1517+ 'aud': self.db.config.TRACKER_WEB,
1518+ 'iat': datetime.datetime.utcnow(),
1519+ }
1520+
1521+ lifetime = 0
1522+ if 'lifetime' in input:
1523+ if input['lifetime'].value != 'unlimited':
1524+ try:
1525+ lifetime = datetime.timedelta(seconds=int(input['lifetime'].value))
1526+ except ValueError:
1527+ raise UsageError("Value 'lifetime' must be 'unlimited' or an integer to specify" +
1528+ " lifetime in seconds. Got %s."%input['lifetime'].value)
1529+ else:
1530+ lifetime = datetime.timedelta(seconds=86400) # 1 day by default
1531+
1532+ if lifetime: # if lifetime = 0 make unlimited by omitting exp claim
1533+ claim['exp'] = datetime.datetime.utcnow() + lifetime
1534+
1535+ newroles = []
1536+ if 'roles' in input:
1537+ for role in input['roles'].value:
1538+ if role not in rolenames:
1539+ raise UsageError("Role %s is not valid."%role)
1540+ if role in user_roles:
1541+ newroles.append(role)
1542+ continue
1543+ parentrole = role.split(':', 1)[0]
1544+ if parentrole in user_roles:
1545+ newroles.append(role)
1546+ continue
1547+
1548+ raise UsageError("Role %s is not permitted."%role)
1549+
1550+ claim['roles'] = newroles
1551+ else:
1552+ claim['roles'] = user_roles
1553+ secret = self.db.config.WEB_JWT_SECRET
1554+ myjwt = jwt.encode(claim, secret, algorithm='HS256')
1555+
1556+ result = {"jwt": b2s(myjwt),
1557+ }
1558+
1559+ return 200, result
1560+
1561+ @Routing.route("/jwt/validate", 'GET')
1562+ @_data_decorator
1563+ def validate_jwt(self,input):
1564+ import jwt
1565+ if not 'jwt' in input:
1566+ raise UsageError("jwt key must be specified")
1567+
1568+ myjwt = input['jwt'].value
1569+
1570+ secret = self.db.config.WEB_JWT_SECRET
1571+ try:
1572+ result = jwt.decode(myjwt, secret,
1573+ algorithms=['HS256'],
1574+ audience=self.db.config.TRACKER_WEB,
1575+ issuer=self.db.config.TRACKER_WEB,
1576+ )
1577+ except jwt.exceptions.InvalidTokenError as err:
1578+ return 401, str(err)
1579+
1580+ return 200, result
15801581
15811582**Note this is sample code. Use at your own risk.** It breaks a few
15821583rules about jwts (e.g. it allows you to make unlimited lifetime
0 commit comments