Python 2.7 is very deprecated on AWS Lambda now

Just over a year after doing this work and posting this, Amazon announced full deprecation dates for Python 2.7 on Lambda. No new Lambda functions using Python 2.7 can be created after July 15, 2021. No existing Lambda functions using Python 2.7 can be updated after September 30, 2021. Maybe try packaging it up in a Docker container if you still need this?

Read-only Moin 1.9.x on AWS Lambda

About this HowTo

MoinMoin versions
1.9.10
Platforms
AWS Lambda

After retiring a fifteen year old Moin install, I set up a read-only copy on AWS Lambda to privately preserve it in full fidelity without having the overhead of managing or maintaining a server. It should run inexpensively, behind HTTP basic access authentication and SSL, for as long as Amazon supports Python 2.7.

Caveats

Setting up an Amazon Web Services account is out of scope for this document. Choosing a serverless framework is out of scope for this document. Creating a local, working Python 2.7 environment for setup and testing is out of scope for this document.

Requirements

zappa_settings.json

   1 {
   2     "wiki": {
   3         "app_function": "moin_wsgi.application", 
   4         "profile_name": "YOUR-PROFILE-NAME", 
   5         "project_name": "YOUR-WIKI-NAME", 
   6         "runtime": "python2.7", 
   7         "s3_bucket": "YOUR-S3-BUCKET",
   8         "aws_region": "YOUR-AWS-REGION", 
   9         "keep_warm": false,
  10         "slim_handler": true,
  11         "authorizer": { "function": "lambda-authorizer-basic-auth.lambda_handler" },
  12     }
  13 }

wikiconfig_local.py

   1 from wikiconfig import LocalConfig
   2 import os
   3 from MoinMoin.config import multiconfig, url_prefix_static
   4 
   5 class Config(LocalConfig):
   6     os.environ['SCRIPT_NAME'] = '/wiki'
   7     url_prefix_static = '/wiki' + url_prefix_static
   8     logo_string = u'<img src="%s/common/moinmoin.png" alt="MoinMoin Logo">' % url_prefix_static
   9     actions_excluded = multiconfig.DefaultConfig.actions_excluded + ['newaccount']
  10     page_front_page = u'YOUR-FRONT-PAGE-NAME' # change to some better value
  11     sitename = u'YOUR-SITE-NAME'
  12     DesktopEdition = False # give all local users full powers (every user is superuser)
  13     acl_rights_default = u"All:read"
  14     secrets = 'YOUR-SECRET-STRING'

lambda-authorizer-basic-auth.py

Derived from https://github.com/dougalb/lambda-authorizer-basic-auth/blob/master/lambda_authorizer_basic_auth/app.py

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

Example console output

   1 $ zappa update wiki
   2 Calling update for stage wiki..
   3 Downloading and installing dependencies..
   4 Packaging project as gzipped tarball.
   5 DEPRECATION: Python 2.7 will reach the end of its life on January 1st, 2020. Please upgrade your Python as Python 2.7 won't be maintained after that date. A future version of pip will drop support for Python 2.7. More details about Python 2 support in pip, can be found at https://pip.pypa.io/en/latest/development/release-process/#python-2-support
   6 WARNING: Target directory ██████████/test04/handler_venv/lib/python2.7/site-packages/zappa already exists. Specify --upgrade to force replacement.
   7 WARNING: Target directory ██████████/test04/handler_venv/lib/python2.7/site-packages/zappa-0.48.2.dist-info already exists. Specify --upgrade to force replacement.
   8 Downloading and installing dependencies..
   9 Packaging project as zip.
  10 Uploading test04-wiki-1578451170.tar.gz (107.2MiB)..
  11 100%|████████████████████████████████████████| 112M/112M [00:41<00:00, 2.01MB/s]
  12 Uploading handler_test04-wiki-1578451262.zip (11.5MiB)..
  13 100%|██████████████████████████████████████| 12.1M/12.1M [00:04<00:00, 2.19MB/s]
  14 Updating Lambda function code..
  15 Updating Lambda function configuration..
  16 Uploading test04-wiki-template-1578451329.json (2.1KiB)..
  17 100%|██████████████████████████████████████| 2.15K/2.15K [00:00<00:00, 15.2KB/s]
  18 Deploying API Gateway..
  19 Your updated Zappa deployment is live!: https://██████████.execute-api.us-east-1.amazonaws.com/wiki
  20 ...
  21 

MoinMoin: HowTo/AwsLambda (last edited 2021-04-02 14:50:12 by VitoMiliano)