· 5 years ago · Jun 02, 2020, 09:24 AM
1from __future__ import print_function
2
3import re
4
5
6def lambda_handler(event, context):
7 print("Client token: " + event['authorizationToken'])
8 print("Method ARN: " + event['methodArn'])
9
10 '''
11 Validate the incoming token and produce the principal user identifier
12 associated with the token. This can be accomplished in a number of ways:
13
14 1. Call out to the OAuth provider
15 2. Decode a JWT token inline
16 3. Lookup in a self-managed DB
17 '''
18 principalId = 'user|a1b2c3d4'
19
20 '''
21 You can send a 401 Unauthorized response to the client by failing like so:
22
23 raise Exception('Unauthorized')
24
25 If the token is valid, a policy must be generated which will allow or deny
26 access to the client. If access is denied, the client will receive a 403
27 Access Denied response. If access is allowed, API Gateway will proceed with
28 the backend integration configured on the method that was called.
29
30 This function must generate a policy that is associated with the recognized
31 principal user identifier. Depending on your use case, you might store
32 policies in a DB, or generate them on the fly.
33
34 Keep in mind, the policy is cached for 5 minutes by default (TTL is
35 configurable in the authorizer) and will apply to subsequent calls to any
36 method/resource in the RestApi made with the same token.
37
38 The example policy below denies access to all resources in the RestApi.
39 '''
40 tmp = event['methodArn'].split(':')
41 apiGatewayArnTmp = tmp[5].split('/')
42 awsAccountId = tmp[4]
43
44 policy = AuthPolicy(principalId, awsAccountId)
45 policy.restApiId = apiGatewayArnTmp[0]
46 policy.region = tmp[3]
47 policy.stage = apiGatewayArnTmp[1]
48 policy.denyAllMethods()
49 #policy.allowMethod(HttpVerb.GET, '/pets/*')
50
51 # Finally, build the policy
52 authResponse = policy.build()
53
54 # new! -- add additional key-value pairs associated with the authenticated principal
55 # these are made available by APIGW like so: $context.authorizer.<key>
56 # additional context is cached
57 context = {
58 'key': 'value', # $context.authorizer.key -> value
59 'number': 1,
60 'bool': True
61 }
62 # context['arr'] = ['foo'] <- this is invalid, APIGW will not accept it
63 # context['obj'] = {'foo':'bar'} <- also invalid
64
65 authResponse['context'] = context
66
67 return authResponse
68
69
70class HttpVerb:
71 GET = 'GET'
72 POST = 'POST'
73 PUT = 'PUT'
74 PATCH = 'PATCH'
75 HEAD = 'HEAD'
76 DELETE = 'DELETE'
77 OPTIONS = 'OPTIONS'
78 ALL = '*'
79
80
81class AuthPolicy(object):
82 # The AWS account id the policy will be generated for. This is used to create the method ARNs.
83 awsAccountId = ''
84 # The principal used for the policy, this should be a unique identifier for the end user.
85 principalId = ''
86 # The policy version used for the evaluation. This should always be '2012-10-17'
87 version = '2012-10-17'
88 # The regular expression used to validate resource paths for the policy
89 pathRegex = '^[/.a-zA-Z0-9-\*]+$'
90
91 '''Internal lists of allowed and denied methods.
92
93 These are lists of objects and each object has 2 properties: A resource
94 ARN and a nullable conditions statement. The build method processes these
95 lists and generates the approriate statements for the final policy.
96 '''
97 allowMethods = []
98 denyMethods = []
99
100 # The API Gateway API id. By default this is set to '*'
101 restApiId = '*'
102 # The region where the API is deployed. By default this is set to '*'
103 region = '*'
104 # The name of the stage used in the policy. By default this is set to '*'
105 stage = '*'
106
107 def __init__(self, principal, awsAccountId):
108 self.awsAccountId = awsAccountId
109 self.principalId = principal
110 self.allowMethods = []
111 self.denyMethods = []
112
113 def _addMethod(self, effect, verb, resource, conditions):
114 '''Adds a method to the internal lists of allowed or denied methods. Each object in
115 the internal list contains a resource ARN and a condition statement. The condition
116 statement can be null.'''
117 if verb != '*' and not hasattr(HttpVerb, verb):
118 raise NameError('Invalid HTTP verb ' + verb + '. Allowed verbs in HttpVerb class')
119 resourcePattern = re.compile(self.pathRegex)
120 if not resourcePattern.match(resource):
121 raise NameError('Invalid resource path: ' + resource + '. Path should match ' + self.pathRegex)
122
123 if resource[:1] == '/':
124 resource = resource[1:]
125
126 resourceArn = 'arn:aws:execute-api:{}:{}:{}/{}/{}/{}'.format(self.region, self.awsAccountId, self.restApiId, self.stage, verb, resource)
127
128 if effect.lower() == 'allow':
129 self.allowMethods.append({
130 'resourceArn': resourceArn,
131 'conditions': conditions
132 })
133 elif effect.lower() == 'deny':
134 self.denyMethods.append({
135 'resourceArn': resourceArn,
136 'conditions': conditions
137 })
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
148 return statement
149
150 def _getStatementForEffect(self, effect, methods):
151 '''This function loops over an array of objects containing a resourceArn and
152 conditions statement and generates the array of statements for the policy.'''
153 statements = []
154
155 if len(methods) > 0:
156 statement = self._getEmptyStatement(effect)
157
158 for curMethod in methods:
159 if curMethod['conditions'] is None or len(curMethod['conditions']) == 0:
160 statement['Resource'].append(curMethod['resourceArn'])
161 else:
162 conditionalStatement = self._getEmptyStatement(effect)
163 conditionalStatement['Resource'].append(curMethod['resourceArn'])
164 conditionalStatement['Condition'] = curMethod['conditions']
165 statements.append(conditionalStatement)
166
167 if statement['Resource']:
168 statements.append(statement)
169
170 return statements
171
172 def allowAllMethods(self):
173 '''Adds a '*' allow to the policy to authorize access to all methods of an API'''
174 self._addMethod('Allow', HttpVerb.ALL, '*', [])
175
176 def denyAllMethods(self):
177 '''Adds a '*' allow to the policy to deny access to all methods of an API'''
178 self._addMethod('Deny', HttpVerb.ALL, '*', [])
179
180 def allowMethod(self, verb, resource):
181 '''Adds an API Gateway method (Http verb + Resource path) to the list of allowed
182 methods for the policy'''
183 self._addMethod('Allow', verb, resource, [])
184
185 def denyMethod(self, verb, resource):
186 '''Adds an API Gateway method (Http verb + Resource path) to the list of denied
187 methods for the policy'''
188 self._addMethod('Deny', verb, resource, [])
189
190 def allowMethodWithConditions(self, verb, resource, conditions):
191 '''Adds an API Gateway method (Http verb + Resource path) to the list of allowed
192 methods and includes a condition for the policy statement. More on AWS policy
193 conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition'''
194 self._addMethod('Allow', verb, resource, conditions)
195
196 def denyMethodWithConditions(self, verb, resource, conditions):
197 '''Adds an API Gateway method (Http verb + Resource path) to the list of denied
198 methods and includes a condition for the policy statement. More on AWS policy
199 conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition'''
200 self._addMethod('Deny', verb, resource, conditions)
201
202 def build(self):
203 '''Generates the policy document based on the internal lists of allowed and denied
204 conditions. This will generate a policy with two main statements for the effect:
205 one statement for Allow and one statement for Deny.
206 Methods that includes conditions will have their own statement in the policy.'''
207 if ((self.allowMethods is None or len(self.allowMethods) == 0) and
208 (self.denyMethods is None or len(self.denyMethods) == 0)):
209 raise NameError('No statements defined for the policy')
210
211 policy = {
212 'principalId': self.principalId,
213 'policyDocument': {
214 'Version': self.version,
215 'Statement': []
216 }
217 }
218
219 policy['policyDocument']['Statement'].extend(self._getStatementForEffect('Allow', self.allowMethods))
220 policy['policyDocument']['Statement'].extend(self._getStatementForEffect('Deny', self.denyMethods))
221
222 return policy