Api-Gateway-Lambda-Auth.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. """
  2. Copyright 2018 Amazon.com, Inc. and its affiliates. All Rights Reserved.
  3. Licensed under the MIT License. See the LICENSE accompanying this file
  4. for the specific language governing permissions and limitations under
  5. the License.
  6. """
  7. from __future__ import print_function
  8. import os
  9. import re
  10. import json
  11. import logging
  12. import base64
  13. import boto3
  14. from aws_xray_sdk.core import xray_recorder
  15. from aws_xray_sdk.core import patch_all
  16. patch_all()
  17. # Static code used for DynamoDB connection and logging
  18. dynamodb = boto3.resource('dynamodb')
  19. table = dynamodb.Table(os.environ['TABLE_NAME'])
  20. log_level = os.environ['LOG_LEVEL']
  21. log = logging.getLogger(__name__)
  22. logging.getLogger().setLevel(log_level)
  23. def lambda_handler(event, context):
  24. log.debug("Event: " + json.dumps(event))
  25. # Ensure the incoming Lambda event is for a request authorizer
  26. if event['type'] != 'REQUEST':
  27. raise Exception('Unauthorized')
  28. try:
  29. # Set the principalId to the accountId making the authorizer call
  30. principalId = event['requestContext']['accountId']
  31. tmp = event['methodArn'].split(':')
  32. apiGatewayArnTmp = tmp[5].split('/')
  33. awsAccountId = tmp[4]
  34. policy = AuthPolicy(principalId, awsAccountId)
  35. policy.restApiId = apiGatewayArnTmp[0]
  36. policy.region = tmp[3]
  37. policy.stage = apiGatewayArnTmp[1]
  38. # Get authorization header in lowercase
  39. authorization_header = {k.lower(): v for k, v in event['headers'].items() if k.lower() == 'authorization'}
  40. log.debug("authorization: " + json.dumps(authorization_header))
  41. # Get the username:password hash from the authorization header
  42. username_password_hash = authorization_header['authorization'].split()[1]
  43. log.debug("username_password_hash: " + username_password_hash)
  44. # Decode username_password_hash and get username
  45. username = base64.standard_b64decode(username_password_hash).split(':')[0]
  46. log.debug("username: " + username)
  47. # Get the password from DynamoDB for the username
  48. item = table.get_item(ConsistentRead=True, Key={"username": username})
  49. if item.get('Item') is not None:
  50. log.debug("item: " + json.dumps(item))
  51. ddb_password = item.get('Item').get('password')
  52. log.debug("ddb_password:" + json.dumps(ddb_password))
  53. if ddb_password is not None:
  54. ddb_username_password = (username + ":" + ddb_password)
  55. ddb_username_password_hash = base64.standard_b64encode(ddb_username_password)
  56. log.debug("ddb_username_password_hash:" + ddb_username_password_hash)
  57. if username_password_hash == ddb_username_password_hash:
  58. policy.allowMethod(event['requestContext']['httpMethod'], event['path'])
  59. log.info("password match for: " + username)
  60. else:
  61. policy.denyMethod(event['requestContext']['httpMethod'], event['path'])
  62. log.info("password does not match for: " + username)
  63. else:
  64. log.info("No password found for username:" + username)
  65. policy.denyMethod(event['requestContext']['httpMethod'], event['path'])
  66. else:
  67. log.info("Did not find username: " + username)
  68. policy.denyMethod(event['requestContext']['httpMethod'], event['path'])
  69. # Finally, build the policy
  70. authResponse = policy.build()
  71. log.debug("authResponse: " + json.dumps(authResponse))
  72. return authResponse
  73. except Exception:
  74. raise Exception('Unauthorized')
  75. class HttpVerb:
  76. GET = "GET"
  77. POST = "POST"
  78. PUT = "PUT"
  79. PATCH = "PATCH"
  80. HEAD = "HEAD"
  81. DELETE = "DELETE"
  82. OPTIONS = "OPTIONS"
  83. ALL = "*"
  84. class AuthPolicy(object):
  85. awsAccountId = ""
  86. """The AWS account id the policy will be generated for. This is used to create the method ARNs."""
  87. principalId = ""
  88. """The principal used for the policy, this should be a unique identifier for the end user."""
  89. version = "2012-10-17"
  90. """The policy version used for the evaluation. This should always be '2012-10-17'"""
  91. pathRegex = "^[/.a-zA-Z0-9-\*]+$"
  92. """The regular expression used to validate resource paths for the policy"""
  93. """these are the internal lists of allowed and denied methods. These are lists
  94. of objects and each object has 2 properties: A resource ARN and a nullable
  95. conditions statement.
  96. the build method processes these lists and generates the approriate
  97. statements for the final policy"""
  98. allowMethods = []
  99. denyMethods = []
  100. restApiId = "*"
  101. """The API Gateway API id. By default this is set to '*'"""
  102. region = "*"
  103. """The region where the API is deployed. By default this is set to '*'"""
  104. stage = "*"
  105. """The name of the stage used in the policy. By default this is set to '*'"""
  106. def __init__(self, principal, awsAccountId):
  107. self.awsAccountId = awsAccountId
  108. self.principalId = principal
  109. self.allowMethods = []
  110. self.denyMethods = []
  111. def _addMethod(self, effect, verb, resource, conditions):
  112. """Adds a method to the internal lists of allowed or denied methods. Each object in
  113. the internal list contains a resource ARN and a condition statement. The condition
  114. statement can be null."""
  115. if verb != "*" and not hasattr(HttpVerb, verb):
  116. raise NameError("Invalid HTTP verb " + verb + ". Allowed verbs in HttpVerb class")
  117. resourcePattern = re.compile(self.pathRegex)
  118. if not resourcePattern.match(resource):
  119. raise NameError("Invalid resource path: " + resource + ". Path should match " + self.pathRegex)
  120. if resource[:1] == "/":
  121. resource = resource[1:]
  122. resourceArn = ("arn:aws:execute-api:" +
  123. self.region + ":" +
  124. self.awsAccountId + ":" +
  125. self.restApiId + "/" +
  126. self.stage + "/" +
  127. verb + "/" +
  128. resource)
  129. if effect.lower() == "allow":
  130. self.allowMethods.append({
  131. 'resourceArn': resourceArn,
  132. 'conditions': conditions
  133. })
  134. elif effect.lower() == "deny":
  135. self.denyMethods.append({
  136. 'resourceArn': resourceArn,
  137. 'conditions': conditions
  138. })
  139. def _getEmptyStatement(self, effect):
  140. """Returns an empty statement object prepopulated with the correct action and the
  141. desired effect."""
  142. statement = {
  143. 'Action': 'execute-api:Invoke',
  144. 'Effect': effect[:1].upper() + effect[1:].lower(),
  145. 'Resource': []
  146. }
  147. return statement
  148. def _getStatementForEffect(self, effect, methods):
  149. """This function loops over an array of objects containing a resourceArn and
  150. conditions statement and generates the array of statements for the policy."""
  151. statements = []
  152. if len(methods) > 0:
  153. statement = self._getEmptyStatement(effect)
  154. for curMethod in methods:
  155. if curMethod['conditions'] is None or len(curMethod['conditions']) == 0:
  156. statement['Resource'].append(curMethod['resourceArn'])
  157. else:
  158. conditionalStatement = self._getEmptyStatement(effect)
  159. conditionalStatement['Resource'].append(curMethod['resourceArn'])
  160. conditionalStatement['Condition'] = curMethod['conditions']
  161. statements.append(conditionalStatement)
  162. statements.append(statement)
  163. return statements
  164. def allowAllMethods(self):
  165. """Adds a '*' allow to the policy to authorize access to all methods of an API"""
  166. self._addMethod("Allow", HttpVerb.ALL, "*", [])
  167. def denyAllMethods(self):
  168. """Adds a '*' allow to the policy to deny access to all methods of an API"""
  169. self._addMethod("Deny", HttpVerb.ALL, "*", [])
  170. def allowMethod(self, verb, resource):
  171. """Adds an API Gateway method (Http verb + Resource path) to the list of allowed
  172. methods for the policy"""
  173. self._addMethod("Allow", verb, resource, [])
  174. def denyMethod(self, verb, resource):
  175. """Adds an API Gateway method (Http verb + Resource path) to the list of denied
  176. methods for the policy"""
  177. self._addMethod("Deny", verb, resource, [])
  178. def allowMethodWithConditions(self, verb, resource, conditions):
  179. """Adds an API Gateway method (Http verb + Resource path) to the list of allowed
  180. methods and includes a condition for the policy statement. More on AWS policy
  181. conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition"""
  182. self._addMethod("Allow", verb, resource, conditions)
  183. def denyMethodWithConditions(self, verb, resource, conditions):
  184. """Adds an API Gateway method (Http verb + Resource path) to the list of denied
  185. methods and includes a condition for the policy statement. More on AWS policy
  186. conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition"""
  187. self._addMethod("Deny", verb, resource, conditions)
  188. def build(self):
  189. """Generates the policy document based on the internal lists of allowed and denied
  190. conditions. This will generate a policy with two main statements for the effect:
  191. one statement for Allow and one statement for Deny.
  192. Methods that includes conditions will have their own statement in the policy."""
  193. if ((self.allowMethods is None or len(self.allowMethods) == 0) and
  194. (self.denyMethods is None or len(self.denyMethods) == 0)):
  195. raise NameError("No statements defined for the policy")
  196. policy = {
  197. 'principalId': self.principalId,
  198. 'policyDocument': {
  199. 'Version': self.version,
  200. 'Statement': []
  201. }
  202. }
  203. policy['policyDocument']['Statement'].extend(self._getStatementForEffect("Allow", self.allowMethods))
  204. policy['policyDocument']['Statement'].extend(self._getStatementForEffect("Deny", self.denyMethods))
  205. return policy