mirror of
https://github.com/offen/website.git
synced 2024-11-21 16:50:27 +01:00
remove accounts app
This commit is contained in:
parent
45c9b104f5
commit
c9e40d8df5
@ -6,7 +6,6 @@ production_env: &production_env
|
||||
- SCRIPT_HOST=https://script-alpha.offen.dev
|
||||
- AUDITORIUM_HOST=https://auditorium-alpha.offen.dev
|
||||
- VAULT_HOST=https://vault-alpha.offen.dev
|
||||
- ACCOUNTS_HOST=https://accounts-alpha.offen.dev
|
||||
- HOMEPAGE_HOST=https://www.offen.dev
|
||||
- NODE_ENV=production
|
||||
- SECRET_ID_SERVER_CONNECTION_STRING=alpha/server/postgresConnectionString
|
||||
@ -19,7 +18,6 @@ deploy_preconditions: &deploy_preconditions
|
||||
- auditorium
|
||||
- packages
|
||||
- shared
|
||||
- accounts
|
||||
filters:
|
||||
branches:
|
||||
only: /^master$/
|
||||
@ -173,75 +171,6 @@ jobs:
|
||||
name: Run tests
|
||||
command: npm test
|
||||
|
||||
accounts:
|
||||
docker:
|
||||
- image: circleci/python:3.6
|
||||
environment:
|
||||
CONFIG_CLASS: accounts.config.LocalConfig
|
||||
MYSQL_CONNECTION_STRING: mysql://root:circle@127.0.0.1:3306/circle
|
||||
- image: circleci/mysql:5.7
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=circle
|
||||
- MYSQL_DATABASE=circle
|
||||
- MYSQL_HOST=127.0.0.1
|
||||
working_directory: ~/offen/accounts
|
||||
steps:
|
||||
- checkout:
|
||||
path: ~/offen
|
||||
- restore_cache:
|
||||
key: offen-accounts-{{ checksum "requirements.txt" }}-{{ checksum "requirements-dev.txt" }}
|
||||
- run:
|
||||
name: Install dependencies
|
||||
command: |
|
||||
python3 -m venv venv
|
||||
. venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
pip install -r requirements-dev.txt
|
||||
- save_cache:
|
||||
paths:
|
||||
- ~/offen/accounts/venv
|
||||
key: offen-accounts-{{ checksum "requirements.txt" }}-{{ checksum "requirements-dev.txt" }}
|
||||
- run:
|
||||
name: Waiting for MySQL to be ready
|
||||
command: |
|
||||
for i in `seq 1 10`;
|
||||
do
|
||||
nc -z localhost 3306 && echo Success && exit 0
|
||||
echo -n .
|
||||
sleep 1
|
||||
done
|
||||
echo Failed waiting for MySQL && exit 1
|
||||
- run:
|
||||
name: Run tests
|
||||
command: |
|
||||
. venv/bin/activate
|
||||
cp ~/offen/bootstrap.yml .
|
||||
make test-ci
|
||||
|
||||
deploy_python:
|
||||
docker:
|
||||
- image: circleci/python:3.6-node
|
||||
<<: *production_env
|
||||
working_directory: ~/offen
|
||||
steps:
|
||||
- checkout:
|
||||
path: ~/offen
|
||||
- restore_cache:
|
||||
key: offen-deploy-{{ checksum "package.json" }}
|
||||
- run:
|
||||
name: Install dependencies
|
||||
command: npm install
|
||||
- save_cache:
|
||||
paths:
|
||||
- ~/offen/packages/node_modules
|
||||
key: offen-packages-{{ checksum "package.json" }}
|
||||
- run:
|
||||
name: Deploy
|
||||
working_directory: ~/offen/accounts
|
||||
command: |
|
||||
echo "Deploying accounts ..."
|
||||
$(npm bin)/sls deploy
|
||||
|
||||
deploy_golang:
|
||||
docker:
|
||||
- image: circleci/golang:1.12-node
|
||||
@ -412,8 +341,6 @@ workflows:
|
||||
<<: *build_preconditions
|
||||
- shared:
|
||||
<<: *build_preconditions
|
||||
- accounts:
|
||||
<<: *build_preconditions
|
||||
- deploy_golang:
|
||||
<<: *deploy_preconditions
|
||||
- deploy_node:
|
||||
|
8
accounts/.gitignore
vendored
8
accounts/.gitignore
vendored
@ -1,8 +0,0 @@
|
||||
*.pem
|
||||
*.dot
|
||||
__pycache__
|
||||
*.log
|
||||
.vscode/
|
||||
.pytest_cache
|
||||
venv/
|
||||
*.pyc
|
@ -1,13 +0,0 @@
|
||||
test:
|
||||
@pytest --disable-pytest-warnings
|
||||
|
||||
test-ci: bootstrap
|
||||
@pytest --disable-pytest-warnings
|
||||
|
||||
fmt:
|
||||
@black .
|
||||
|
||||
bootstrap:
|
||||
@python -m scripts.bootstrap
|
||||
|
||||
.PHONY: test fmt bootstrap
|
@ -1,37 +0,0 @@
|
||||
# accounts
|
||||
|
||||
The `accounts` app is responsible for managing operator accounts and issuing authentication tokens that will identify requests made to the `server` and the `kms` service.
|
||||
|
||||
The application is built using the [Flask][flask-docs] framework.
|
||||
|
||||
[flask-docs]: http://flask.pocoo.org/
|
||||
|
||||
---
|
||||
|
||||
## Development
|
||||
|
||||
### Commands
|
||||
|
||||
#### Install dependencies
|
||||
|
||||
```
|
||||
pip install --user -r requirements.txt -r requirements-dev.txt
|
||||
```
|
||||
|
||||
#### Run the development server
|
||||
|
||||
```
|
||||
FLASK_APP=accounts FLASK_ENV=development flask run
|
||||
```
|
||||
|
||||
#### Run the tests
|
||||
|
||||
```
|
||||
make test
|
||||
```
|
||||
|
||||
#### Auto format code using `black`
|
||||
|
||||
```
|
||||
make fmt
|
||||
```
|
@ -1,25 +0,0 @@
|
||||
from os import environ
|
||||
|
||||
from flask import Flask
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_admin import Admin
|
||||
from werkzeug.utils import import_string
|
||||
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
cfg = import_string(environ.get("CONFIG_CLASS"))()
|
||||
app.config.from_object(cfg)
|
||||
|
||||
db = SQLAlchemy(app)
|
||||
|
||||
from accounts.models import Account, User
|
||||
from accounts.views import AccountView, UserView
|
||||
import accounts.api
|
||||
|
||||
admin = Admin(
|
||||
app, name="offen admin", template_mode="bootstrap3", base_template="index.html"
|
||||
)
|
||||
|
||||
admin.add_view(AccountView(Account, db.session))
|
||||
admin.add_view(UserView(User, db.session))
|
@ -1,134 +0,0 @@
|
||||
from datetime import datetime, timedelta
|
||||
from functools import wraps
|
||||
|
||||
from flask import jsonify, make_response, request
|
||||
from flask_cors import cross_origin
|
||||
from passlib.hash import bcrypt
|
||||
import jwt
|
||||
|
||||
from accounts import app
|
||||
from accounts.models import User
|
||||
|
||||
COOKIE_KEY = "auth"
|
||||
|
||||
|
||||
def json_error(handler):
|
||||
@wraps(handler)
|
||||
def wrapped_handler(*args, **kwargs):
|
||||
try:
|
||||
return handler(*args, **kwargs)
|
||||
except Exception as server_error:
|
||||
return (
|
||||
jsonify(
|
||||
{
|
||||
"error": "Internal server error: {}".format(str(server_error)),
|
||||
"status": 500,
|
||||
}
|
||||
),
|
||||
500,
|
||||
)
|
||||
|
||||
return wrapped_handler
|
||||
|
||||
|
||||
class UnauthorizedError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@app.route("/api/login", methods=["POST"])
|
||||
@cross_origin(origins=[app.config["CORS_ORIGIN"]], supports_credentials=True)
|
||||
@json_error
|
||||
def post_login():
|
||||
credentials = request.get_json(force=True)
|
||||
try:
|
||||
match = User.query.filter_by(email=credentials["username"]).first()
|
||||
if not match:
|
||||
raise UnauthorizedError("bad username")
|
||||
if not bcrypt.verify(credentials["password"], match.hashed_password):
|
||||
raise UnauthorizedError("bad password")
|
||||
except UnauthorizedError as unauthorized_error:
|
||||
resp = make_response(jsonify({"error": str(unauthorized_error), "status": 401}))
|
||||
resp.set_cookie(COOKIE_KEY, "", expires=0)
|
||||
resp.status_code = 401
|
||||
return resp
|
||||
|
||||
expiry = datetime.utcnow() + timedelta(hours=24)
|
||||
encoded = jwt.encode(
|
||||
{
|
||||
"exp": expiry,
|
||||
"priv": {
|
||||
"userId": match.user_id,
|
||||
"accounts": [a.account_id for a in match.accounts],
|
||||
},
|
||||
},
|
||||
app.config["JWT_PRIVATE_KEY"].encode(),
|
||||
algorithm="RS256",
|
||||
).decode()
|
||||
|
||||
resp = make_response(jsonify({"user": match.serialize()}))
|
||||
resp.set_cookie(
|
||||
COOKIE_KEY,
|
||||
encoded,
|
||||
httponly=True,
|
||||
expires=expiry,
|
||||
path="/",
|
||||
domain=app.config["COOKIE_DOMAIN"],
|
||||
samesite="strict",
|
||||
)
|
||||
return resp
|
||||
|
||||
|
||||
@app.route("/api/login", methods=["GET"])
|
||||
@cross_origin(origins=[app.config["CORS_ORIGIN"]], supports_credentials=True)
|
||||
@json_error
|
||||
def get_login():
|
||||
auth_cookie = request.cookies.get(COOKIE_KEY)
|
||||
if not auth_cookie:
|
||||
return jsonify({"error": "no auth cookie in request", "status": 401}), 401
|
||||
|
||||
public_keys = app.config["JWT_PUBLIC_KEYS"]
|
||||
|
||||
token = None
|
||||
token_err = None
|
||||
for public_key in public_keys:
|
||||
try:
|
||||
token = jwt.decode(auth_cookie, public_key)
|
||||
break
|
||||
except Exception as decode_err:
|
||||
token_err = decode_err
|
||||
|
||||
if not token:
|
||||
return jsonify({"error": str(token_err), "status": 401}), 401
|
||||
|
||||
try:
|
||||
match = User.query.get(token["priv"]["userId"])
|
||||
except KeyError as key_err:
|
||||
return (
|
||||
jsonify(
|
||||
{"error": "malformed JWT claims: {}".format(key_err), "status": 401}
|
||||
),
|
||||
401,
|
||||
)
|
||||
if match:
|
||||
return jsonify({"user": match.serialize()})
|
||||
return jsonify({"error": "unknown user id", "status": 401}), 401
|
||||
|
||||
|
||||
@app.route("/api/logout", methods=["POST"])
|
||||
@cross_origin(origins=[app.config["CORS_ORIGIN"]], supports_credentials=True)
|
||||
@json_error
|
||||
def post_logout():
|
||||
resp = make_response("")
|
||||
resp.set_cookie(COOKIE_KEY, "", expires=0)
|
||||
resp.status_code = 204
|
||||
return resp
|
||||
|
||||
|
||||
@app.route("/api/key", methods=["GET"])
|
||||
@json_error
|
||||
def key():
|
||||
"""
|
||||
This route is not supposed to be called by client-side applications, so
|
||||
no CORS configuration is added
|
||||
"""
|
||||
return jsonify({"keys": app.config["JWT_PUBLIC_KEYS"]})
|
@ -1,73 +0,0 @@
|
||||
import json
|
||||
from os import environ
|
||||
|
||||
|
||||
class BaseConfig(object):
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
FLASK_ADMIN_SWATCH = "flatly"
|
||||
|
||||
|
||||
class LocalConfig(BaseConfig):
|
||||
SECRET_KEY = environ.get("SESSION_SECRET")
|
||||
SQLALCHEMY_DATABASE_URI = environ.get("MYSQL_CONNECTION_STRING")
|
||||
CORS_ORIGIN = environ.get("CORS_ORIGIN", "*")
|
||||
COOKIE_DOMAIN = environ.get("COOKIE_DOMAIN")
|
||||
SERVER_HOST = environ.get("SERVER_HOST")
|
||||
def __init__(self):
|
||||
with open("private_key.pem") as f:
|
||||
private_key = f.read()
|
||||
with open("public_key.pem") as f:
|
||||
public_key = f.read()
|
||||
self.JWT_PRIVATE_KEY = private_key
|
||||
self.JWT_PUBLIC_KEYS = [public_key]
|
||||
|
||||
|
||||
class SecretsManagerConfig(BaseConfig):
|
||||
CORS_ORIGIN = environ.get("CORS_ORIGIN", "*")
|
||||
COOKIE_DOMAIN = environ.get("COOKIE_DOMAIN")
|
||||
SERVER_HOST = environ.get("SERVER_HOST")
|
||||
|
||||
def __init__(self):
|
||||
import boto3
|
||||
|
||||
session = boto3.session.Session()
|
||||
self.client = session.client(
|
||||
service_name="secretsmanager", region_name=environ.get("AWS_REGION")
|
||||
)
|
||||
|
||||
self.SECRET_KEY = self.get_secret("sessionSecret")
|
||||
self.SQLALCHEMY_DATABASE_URI = self.get_secret("mysqlConnectionString")
|
||||
|
||||
current_version = self.get_secret("jwtKeyPair")
|
||||
key_pair = json.loads(current_version)
|
||||
previous_version = self.get_secret("jwtKeyPair", previous=True)
|
||||
previous_key_pair = (
|
||||
json.loads(previous_version)
|
||||
if previous_version is not None
|
||||
else {"public": None}
|
||||
)
|
||||
|
||||
self.JWT_PRIVATE_KEY = key_pair["private"]
|
||||
self.JWT_PUBLIC_KEYS = [
|
||||
k for k in [key_pair["public"], previous_key_pair["public"]] if k
|
||||
]
|
||||
|
||||
def get_secret(self, secret_name, previous=False):
|
||||
import base64
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
try:
|
||||
ssm_response = self.client.get_secret_value(
|
||||
SecretId="{}/accounts/{}".format(environ.get("STAGE"), secret_name),
|
||||
VersionStage=("AWSPREVIOUS" if previous else "AWSCURRENT"),
|
||||
)
|
||||
except ClientError as e:
|
||||
if e.response["Error"]["Code"] == "ResourceNotFoundException" and previous:
|
||||
# A secret might not have a previous version yet. It is left
|
||||
# up to the caller to handle the None return in this case
|
||||
return None
|
||||
raise e
|
||||
|
||||
if "SecretString" in ssm_response:
|
||||
return ssm_response["SecretString"]
|
||||
return base64.b64decode(ssm_response["SecretBinary"])
|
@ -1,49 +0,0 @@
|
||||
from uuid import uuid4
|
||||
|
||||
from accounts import db
|
||||
|
||||
|
||||
def generate_key():
|
||||
return str(uuid4())
|
||||
|
||||
|
||||
class Account(db.Model):
|
||||
__tablename__ = "accounts"
|
||||
account_id = db.Column(db.String(36), primary_key=True, default=generate_key)
|
||||
name = db.Column(db.Text, nullable=False)
|
||||
users = db.relationship("AccountUserAssociation", back_populates="account", cascade="delete")
|
||||
|
||||
def __repr__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class User(db.Model):
|
||||
__tablename__ = "users"
|
||||
user_id = db.Column(db.String(36), primary_key=True, default=generate_key)
|
||||
email = db.Column(db.String(128), nullable=False, unique=True)
|
||||
hashed_password = db.Column(db.Text, nullable=False)
|
||||
accounts = db.relationship(
|
||||
"AccountUserAssociation", back_populates="user", lazy="joined"
|
||||
)
|
||||
|
||||
def serialize(self):
|
||||
associated_accounts = [a.account_id for a in self.accounts]
|
||||
records = [
|
||||
{"name": a.name, "accountId": a.account_id}
|
||||
for a in Account.query.filter(Account.account_id.in_(associated_accounts))
|
||||
]
|
||||
return {"userId": self.user_id, "email": self.email, "accounts": records}
|
||||
|
||||
|
||||
class AccountUserAssociation(db.Model):
|
||||
__tablename__ = "account_to_user"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
user_id = db.Column(db.String(36), db.ForeignKey("users.user_id"), nullable=False)
|
||||
account_id = db.Column(
|
||||
db.String(36), db.ForeignKey("accounts.account_id"), nullable=False
|
||||
)
|
||||
|
||||
user = db.relationship("User", back_populates="accounts")
|
||||
account = db.relationship("Account", back_populates="users")
|
@ -1,17 +0,0 @@
|
||||
{% extends 'admin/base.html' %}
|
||||
|
||||
{% block head_css %}
|
||||
<link href="https://stackpath.bootstrapcdn.com/bootswatch/3.3.5/flatly/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="{{ admin_static.url(filename='admin/css/bootstrap3/admin.css', v='1.1.1') }}" rel="stylesheet">
|
||||
<link href="{{ admin_static.url(filename='admin/css/bootstrap3/submenu.css') }}" rel="stylesheet">
|
||||
{% if admin_view.extra_css %}
|
||||
{% for css_url in admin_view.extra_css %}
|
||||
<link href="{{ css_url }}" rel="stylesheet">
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<style>
|
||||
body {
|
||||
padding-top: 4px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
@ -1,122 +0,0 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import requests
|
||||
from flask_admin.contrib.sqla import ModelView
|
||||
from wtforms import PasswordField, StringField, Form
|
||||
from wtforms.validators import InputRequired, EqualTo
|
||||
from passlib.hash import bcrypt
|
||||
import jwt
|
||||
|
||||
from accounts import db, app
|
||||
from accounts.models import AccountUserAssociation
|
||||
|
||||
|
||||
class RemoteServerException(Exception):
|
||||
status = 0
|
||||
|
||||
def __str__(self):
|
||||
return "Status {}: {}".format(
|
||||
self.status, super(RemoteServerException, self).__str__()
|
||||
)
|
||||
|
||||
|
||||
def _call_remote_server(account_id, method):
|
||||
# expires in 30 seconds as this will mean the HTTP request would have
|
||||
# timed out anyways
|
||||
expiry = datetime.utcnow() + timedelta(seconds=30)
|
||||
encoded = jwt.encode(
|
||||
{"ok": True, "exp": expiry, "priv": {"rpc": "1"}},
|
||||
app.config["JWT_PRIVATE_KEY"].encode(),
|
||||
algorithm="RS256",
|
||||
).decode("utf-8")
|
||||
|
||||
do_request = None
|
||||
if method == "POST":
|
||||
do_request = requests.post
|
||||
elif method == "DELETE":
|
||||
do_request = requests.delete
|
||||
|
||||
if not do_request:
|
||||
raise Exception("Received unsupported method {}, cannot continue.".format(method))
|
||||
|
||||
r = do_request(
|
||||
"{}/accounts".format(app.config["SERVER_HOST"]),
|
||||
json={"accountId": account_id},
|
||||
headers={"X-RPC-Authentication": encoded},
|
||||
)
|
||||
|
||||
if r.status_code > 299:
|
||||
err = r.json()
|
||||
remote_err = RemoteServerException(err["error"])
|
||||
remote_err.status = err["status"]
|
||||
raise remote_err
|
||||
|
||||
|
||||
def create_remote_account(account_id):
|
||||
return _call_remote_server(account_id, "POST")
|
||||
|
||||
|
||||
def retire_remote_account(account_id):
|
||||
return _call_remote_server(account_id, "DELETE")
|
||||
|
||||
|
||||
class AccountForm(Form):
|
||||
name = StringField(
|
||||
"Account Name",
|
||||
validators=[InputRequired()],
|
||||
description="This is the account name visible to users",
|
||||
)
|
||||
|
||||
|
||||
class AccountView(ModelView):
|
||||
form = AccountForm
|
||||
column_display_all_relations = True
|
||||
column_list = ("name", "account_id")
|
||||
|
||||
def after_model_change(self, form, model, is_created):
|
||||
if is_created:
|
||||
try:
|
||||
create_remote_account(model.account_id)
|
||||
except RemoteServerException as server_error:
|
||||
db.session.delete(model)
|
||||
db.session.commit()
|
||||
raise server_error
|
||||
|
||||
def after_model_delete(self, model):
|
||||
retire_remote_account(model.account_id)
|
||||
|
||||
|
||||
class UserView(ModelView):
|
||||
inline_models = [(AccountUserAssociation, dict(form_columns=["id", "account"]))]
|
||||
column_auto_select_related = True
|
||||
column_display_all_relations = True
|
||||
column_list = ("email", "user_id")
|
||||
form_columns = ("email", "accounts")
|
||||
form_create_rules = ("email", "password", "confirm", "accounts")
|
||||
form_edit_rules = ("email", "password", "confirm", "accounts")
|
||||
|
||||
def on_model_change(self, form, model, is_created):
|
||||
if form.password.data:
|
||||
model.hashed_password = bcrypt.hash(form.password.data)
|
||||
|
||||
def get_create_form(self):
|
||||
form = super(UserView, self).get_create_form()
|
||||
form.password = PasswordField(
|
||||
"Password",
|
||||
validators=[
|
||||
InputRequired(),
|
||||
EqualTo("confirm", message="Passwords must match"),
|
||||
],
|
||||
)
|
||||
form.confirm = PasswordField("Repeat Password", validators=[InputRequired()])
|
||||
return form
|
||||
|
||||
def get_edit_form(self):
|
||||
form = super(UserView, self).get_edit_form()
|
||||
form.password = PasswordField(
|
||||
"Password",
|
||||
description="When left blank, the password will remain unchanged on update",
|
||||
validators=[EqualTo("confirm", message="Passwords must match")],
|
||||
)
|
||||
form.confirm = PasswordField("Repeat Password", validators=[])
|
||||
return form
|
@ -1,80 +0,0 @@
|
||||
import base64
|
||||
from os import environ
|
||||
|
||||
import boto3
|
||||
from botocore.exceptions import ClientError
|
||||
from passlib.hash import bcrypt
|
||||
|
||||
|
||||
session = boto3.session.Session()
|
||||
boto_client = session.client(
|
||||
service_name="secretsmanager", region_name=environ.get("AWS_REGION")
|
||||
)
|
||||
|
||||
|
||||
def get_secret(boto_client, secret_name):
|
||||
ssm_response = boto_client.get_secret_value(
|
||||
SecretId="{}/accounts/{}".format(environ.get("STAGE"), secret_name)
|
||||
)
|
||||
if "SecretString" in ssm_response:
|
||||
return ssm_response["SecretString"]
|
||||
return base64.b64decode(ssm_response["SecretBinary"])
|
||||
|
||||
|
||||
basic_auth_user = get_secret(boto_client, "basicAuthUser")
|
||||
hashed_basic_auth_password = get_secret(boto_client, "hashedBasicAuthPassword")
|
||||
|
||||
|
||||
def build_api_arn(method_arn):
|
||||
arn_chunks = method_arn.split(":")
|
||||
aws_region = arn_chunks[3]
|
||||
aws_account_id = arn_chunks[4]
|
||||
|
||||
gateway_arn_chunks = arn_chunks[5].split("/")
|
||||
rest_api_id = gateway_arn_chunks[0]
|
||||
stage = gateway_arn_chunks[1]
|
||||
|
||||
return "arn:aws:execute-api:{}:{}:{}/{}/*/*".format(
|
||||
aws_region, aws_account_id, rest_api_id, stage
|
||||
)
|
||||
|
||||
|
||||
def build_response(api_arn, allow):
|
||||
effect = "Deny"
|
||||
if allow:
|
||||
effect = "Allow"
|
||||
|
||||
return {
|
||||
"principalId": "offen",
|
||||
"policyDocument": {
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Action": ["execute-api:Invoke"],
|
||||
"Effect": effect,
|
||||
"Resource": [api_arn],
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def handler(event, context):
|
||||
api_arn = build_api_arn(event["methodArn"])
|
||||
|
||||
encoded_auth = event["authorizationToken"].lstrip("Basic ")
|
||||
auth_string = base64.standard_b64decode(encoded_auth).decode()
|
||||
if not auth_string:
|
||||
return build_response(api_arn, False)
|
||||
|
||||
credentials = auth_string.split(":")
|
||||
user = credentials[0]
|
||||
password = credentials[1]
|
||||
|
||||
if user != basic_auth_user:
|
||||
return build_response(api_arn, False)
|
||||
|
||||
if not bcrypt.verify(password, hashed_basic_auth_password):
|
||||
return build_response(api_arn, False)
|
||||
|
||||
return build_response(api_arn, True)
|
@ -1,21 +0,0 @@
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
|
||||
|
||||
def create_key_pair(**kwargs):
|
||||
key = rsa.generate_private_key(
|
||||
backend=default_backend(), public_exponent=65537, **kwargs
|
||||
)
|
||||
|
||||
public_key = key.public_key().public_bytes(
|
||||
serialization.Encoding.PEM, serialization.PublicFormat.PKCS1
|
||||
)
|
||||
|
||||
pem = key.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.PKCS8,
|
||||
encryption_algorithm=serialization.NoEncryption(),
|
||||
)
|
||||
|
||||
return {"private": pem.decode(), "public": public_key.decode()}
|
@ -1,123 +0,0 @@
|
||||
import io
|
||||
import boto3
|
||||
import logging
|
||||
import json
|
||||
from os import environ
|
||||
|
||||
from lambdas.create_keys import create_key_pair
|
||||
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
|
||||
def handler(event, context):
|
||||
arn = event["SecretId"]
|
||||
token = event["ClientRequestToken"]
|
||||
step = event["Step"]
|
||||
|
||||
session = boto3.session.Session()
|
||||
service_client = session.client(
|
||||
service_name="secretsmanager", region_name=environ.get("AWS_REGION")
|
||||
)
|
||||
|
||||
# Make sure the version is staged correctly
|
||||
metadata = service_client.describe_secret(SecretId=arn)
|
||||
if not metadata["RotationEnabled"]:
|
||||
logger.error("Secret %s is not enabled for rotation" % arn)
|
||||
raise ValueError("Secret %s is not enabled for rotation" % arn)
|
||||
versions = metadata["VersionIdsToStages"]
|
||||
if token not in versions:
|
||||
logger.error(
|
||||
"Secret version %s has no stage for rotation of secret %s.", token, arn
|
||||
)
|
||||
raise ValueError(
|
||||
"Secret version %s has no stage for rotation of secret %s." % (token, arn)
|
||||
)
|
||||
if "AWSCURRENT" in versions[token]:
|
||||
logger.info(
|
||||
"Secret version %s already set as AWSCURRENT for secret %s.", token, arn
|
||||
)
|
||||
return
|
||||
elif "AWSPENDING" not in versions[token]:
|
||||
logger.error(
|
||||
"Secret version %s not set as AWSPENDING for rotation of secret %s.",
|
||||
token,
|
||||
arn,
|
||||
)
|
||||
raise ValueError(
|
||||
"Secret version %s not set as AWSPENDING for rotation of secret %s."
|
||||
% (token, arn)
|
||||
)
|
||||
|
||||
if step == "createSecret":
|
||||
create_secret(service_client, arn, token)
|
||||
|
||||
elif step == "setSecret":
|
||||
set_secret(service_client, arn, token)
|
||||
|
||||
elif step == "testSecret":
|
||||
test_secret(service_client, arn, token)
|
||||
|
||||
elif step == "finishSecret":
|
||||
finish_secret(service_client, arn, token)
|
||||
|
||||
else:
|
||||
raise ValueError("Invalid step parameter")
|
||||
|
||||
|
||||
def create_secret(service_client, arn, token):
|
||||
service_client.get_secret_value(SecretId=arn, VersionStage="AWSCURRENT")
|
||||
try:
|
||||
service_client.get_secret_value(
|
||||
SecretId=arn, VersionId=token, VersionStage="AWSPENDING"
|
||||
)
|
||||
logger.info("createSecret: Successfully retrieved secret for %s." % arn)
|
||||
except service_client.exceptions.ResourceNotFoundException:
|
||||
secret = create_key_pair(key_size=2048)
|
||||
service_client.put_secret_value(
|
||||
SecretId=arn,
|
||||
ClientRequestToken=token,
|
||||
SecretString=json.dumps(secret).encode().decode("unicode_escape"),
|
||||
VersionStages=["AWSPENDING"],
|
||||
)
|
||||
logger.info(
|
||||
"createSecret: Successfully put secret for ARN %s and version %s."
|
||||
% (arn, token)
|
||||
)
|
||||
|
||||
|
||||
def set_secret(service_client, arn, token):
|
||||
pass
|
||||
|
||||
|
||||
def test_secret(service_client, arn, token):
|
||||
pass
|
||||
|
||||
|
||||
def finish_secret(service_client, arn, token):
|
||||
metadata = service_client.describe_secret(SecretId=arn)
|
||||
current_version = None
|
||||
for version in metadata["VersionIdsToStages"]:
|
||||
if "AWSCURRENT" in metadata["VersionIdsToStages"][version]:
|
||||
if version == token:
|
||||
# The correct version is already marked as current, return
|
||||
logger.info(
|
||||
"finishSecret: Version %s already marked as AWSCURRENT for %s",
|
||||
version,
|
||||
arn,
|
||||
)
|
||||
return
|
||||
current_version = version
|
||||
break
|
||||
|
||||
# Finalize by staging the secret version current
|
||||
service_client.update_secret_version_stage(
|
||||
SecretId=arn,
|
||||
VersionStage="AWSCURRENT",
|
||||
MoveToVersionId=token,
|
||||
RemoveFromVersionId=current_version,
|
||||
)
|
||||
logger.info(
|
||||
"finishSecret: Successfully set AWSCURRENT stage to version %s for secret %s."
|
||||
% (token, arn)
|
||||
)
|
@ -1,3 +0,0 @@
|
||||
pytest
|
||||
black
|
||||
pyyaml
|
@ -1,12 +0,0 @@
|
||||
Flask==1.0.2
|
||||
Flask-Admin==1.5.3
|
||||
Flask-Cors==3.0.8
|
||||
Flask-SQLAlchemy==2.4.0
|
||||
werkzeug==0.15.4
|
||||
pyjwt[crypto]==1.7.1
|
||||
passlib==1.7.1
|
||||
bcrypt==3.1.7
|
||||
PyMySQL==0.9.3
|
||||
mysqlclient==1.4.2.post1
|
||||
requests==2.22.0
|
||||
cryptography==2.7
|
@ -1,41 +0,0 @@
|
||||
import yaml
|
||||
from passlib.hash import bcrypt
|
||||
|
||||
from lambdas.create_keys import create_key_pair
|
||||
|
||||
if __name__ == "__main__":
|
||||
keypair = create_key_pair(key_size=2048)
|
||||
with open("public_key.pem", "w") as f:
|
||||
f.write(keypair["public"])
|
||||
|
||||
with open("private_key.pem", "w") as f:
|
||||
f.write(keypair["private"])
|
||||
|
||||
from accounts import db
|
||||
from accounts.models import Account, User, AccountUserAssociation
|
||||
|
||||
db.drop_all()
|
||||
db.create_all()
|
||||
|
||||
with open("./bootstrap.yml", "r") as stream:
|
||||
data = yaml.safe_load(stream)
|
||||
|
||||
for account in data["accounts"]:
|
||||
record = Account(
|
||||
name=account["name"],
|
||||
account_id=account["id"],
|
||||
)
|
||||
db.session.add(record)
|
||||
|
||||
for user in data["users"]:
|
||||
record = User(
|
||||
email=user["email"],
|
||||
hashed_password=bcrypt.hash(user["password"]),
|
||||
)
|
||||
for account_id in user["accounts"]:
|
||||
record.accounts.append(AccountUserAssociation(account_id=account_id))
|
||||
db.session.add(record)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
print("Successfully bootstrapped accounts database and created key pair")
|
@ -1,20 +0,0 @@
|
||||
import base64
|
||||
import argparse
|
||||
|
||||
from passlib.hash import bcrypt
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--password", type=str, help="The password to hash", required=True)
|
||||
parser.add_argument(
|
||||
"--plain",
|
||||
help="Do not encode the result as base64",
|
||||
default=False,
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = parser.parse_args()
|
||||
out = bcrypt.hash(args.password)
|
||||
if not args.plain:
|
||||
out = base64.standard_b64encode(out.encode()).decode()
|
||||
print(out)
|
@ -1,114 +0,0 @@
|
||||
service:
|
||||
name: accounts
|
||||
awsKmsKeyArn: ${ssm:/aws/reference/secretsmanager/${self:custom.stage}/all/kmsArn~true}
|
||||
|
||||
provider:
|
||||
name: aws
|
||||
endpointType: regional
|
||||
runtime: python3.6
|
||||
stage: alpha
|
||||
region: eu-central-1
|
||||
apiName: offen-${self:provider.stage}
|
||||
logs:
|
||||
restApi: true
|
||||
iamRoleStatements:
|
||||
- Effect: 'Allow'
|
||||
Action:
|
||||
- secretsmanager:GetSecretValue
|
||||
Resource: arn:aws:secretsmanager:eu-central-1:#{AWS::AccountId}:secret:${self:custom.stage}/*
|
||||
|
||||
package:
|
||||
individually: true
|
||||
exclude:
|
||||
- tests
|
||||
|
||||
plugins:
|
||||
- serverless-domain-manager
|
||||
- serverless-python-requirements
|
||||
- serverless-wsgi
|
||||
- serverless-pseudo-parameters
|
||||
|
||||
custom:
|
||||
stage: ${opt:stage, self:provider.stage}
|
||||
origin:
|
||||
production: https://vault.offen.dev
|
||||
staging: https://vault-staging.offen.dev
|
||||
alpha: https://vault-alpha.offen.dev
|
||||
serverHost:
|
||||
production: https://server.offen.dev
|
||||
staging: https://server-staging.offen.dev
|
||||
alpha: https://server-alpha.offen.dev
|
||||
domain:
|
||||
production: accounts.offen.dev
|
||||
staging: accounts-staging.offen.dev
|
||||
alpha: accounts-alpha.offen.dev
|
||||
cookieDomain:
|
||||
production: .offen.dev
|
||||
staging: .offen.dev
|
||||
alpha: .offen.dev
|
||||
customDomain:
|
||||
basePath: ''
|
||||
certificateName: '*.offen.dev'
|
||||
domainName: ${self:custom.domain.${self:custom.stage}}
|
||||
stage: ${self:custom.stage}
|
||||
endpointType: regional
|
||||
createRoute53Record: false
|
||||
wsgi:
|
||||
app: accounts.app
|
||||
packRequirements: false
|
||||
pythonRequirements:
|
||||
slim: true
|
||||
dockerizePip: non-linux
|
||||
fileName: requirements.txt
|
||||
|
||||
functions:
|
||||
authorizer:
|
||||
handler: lambdas.authorizer.handler
|
||||
environment:
|
||||
STAGE: ${self:custom.stage}
|
||||
rotateKeys:
|
||||
handler: lambdas.rotate_keys.handler
|
||||
environment:
|
||||
STAGE: ${self:custom.stage}
|
||||
app:
|
||||
handler: wsgi_handler.handler
|
||||
timeout: 30
|
||||
events:
|
||||
- http:
|
||||
path: /admin/
|
||||
method: any
|
||||
authorizer:
|
||||
name: authorizer
|
||||
resultTtlInSeconds: 0
|
||||
identitySource: method.request.header.Authorization
|
||||
- http:
|
||||
path: /admin/{proxy+}
|
||||
method: any
|
||||
authorizer:
|
||||
name: authorizer
|
||||
resultTtlInSeconds: 0
|
||||
identitySource: method.request.header.Authorization
|
||||
- http:
|
||||
path: '/'
|
||||
method: any
|
||||
- http:
|
||||
path: '/{proxy+}'
|
||||
method: any
|
||||
environment:
|
||||
CONFIG_CLASS: accounts.config.SecretsManagerConfig
|
||||
STAGE: ${self:custom.stage}
|
||||
CORS_ORIGIN: ${self:custom.origin.${self:custom.stage}}
|
||||
COOKIE_DOMAIN: ${self:custom.cookieDomain.${self:custom.stage}}
|
||||
SERVER_HOST: ${self:custom.serverHost.${self:custom.stage}}
|
||||
|
||||
resources:
|
||||
Resources:
|
||||
GatewayResponse:
|
||||
Type: 'AWS::ApiGateway::GatewayResponse'
|
||||
Properties:
|
||||
ResponseParameters:
|
||||
gatewayresponse.header.WWW-Authenticate: "'Basic'"
|
||||
ResponseType: UNAUTHORIZED
|
||||
RestApiId:
|
||||
Ref: 'ApiGatewayRestApi'
|
||||
StatusCode: '401'
|
@ -1,206 +0,0 @@
|
||||
import unittest
|
||||
import json
|
||||
import base64
|
||||
from time import time
|
||||
from datetime import datetime, timedelta
|
||||
from os import environ
|
||||
|
||||
import jwt
|
||||
|
||||
from accounts import app
|
||||
|
||||
|
||||
FOREIGN_PRIVATE_KEY = """
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCwAPFiTSLKlVvG
|
||||
N97TIyDWIxPp4Ji8hAmtlMn0gdGclC2DGKA2v7orXdNkngFon0PPe08acKI5NL9P
|
||||
nkVSrjWxrn8H7LeNQadwPxjYVmri4SLhBJUcAe+SoqrIZtrci+2y64mLPrl6wxBj
|
||||
ZKDl8o1Qm8iZSMgJ+wRG2FrItZUBWLZ79KSB2lQkO5OWorPX3T0SPxQXqq9hc4xN
|
||||
6I+qtfmv5jZTJOviMCehOs48ZlObgr/W+Kak4q/jrrqXvG3XQqVVTN/z95+2XuN4
|
||||
Btj7fv24PIRE/BddDAzC/yzISYb9QqLChaxx1fqY+aSA6ou2wh1PjUiyXNnAmP2i
|
||||
6UWwikILAgMBAAECggEBAJuYmc1/x+w00qeQKQubmKH27NnsVtsCF9Q/H7NrOTYl
|
||||
wX6OPMVqBlnkXsgq76/gbQB2UN5dCO1t9lua3kpT/OASFfeZjEPy8OXIwlwvOdtN
|
||||
kZpAhNn31CZcbIMyevZTNlbg5/4T+8HNxSU5hw0Cu2+x6UuqDj7UjVlcWBXsgchn
|
||||
f8kguLHr6Q7rndC10Vv5a4Rz9fzuS2K4jEnhlJjgD22XB2SCH5kLrAikH10AW761
|
||||
5g7HSiMxKSUyXc51PX3n/FkxjzT0Vm1ENeZou263VEQhke49IWLIcbLD7ShOyNaI
|
||||
TuYPAyRY4o70/d/YTydRCEp/H8stB6UaVK9hlzzfoMECgYEA1e9UgW4vBueSoZv3
|
||||
llc7dXlAnk6nJeCaujjPBAd0Sc3XcpMik1kb8oDgI4fwNxTYqlHu3Wp7ZLR14C4G
|
||||
rlry+4rRUdxnWNcKtyOtA6km0b33V3ja4GsLViENBSQZDUe7EljER2VSRynMTog0
|
||||
lfmUr+ORzWDpanEO+Ke25zhU2DsCgYEA0pxM2UjmmAepSWBAcXABjIFE09MxXVTS
|
||||
NwRhdYjHJsKmGnPD8DEDJbRSHNAEN2mTD2kJW5pFThKVWtQ8WpjSXuRSkS7HzXrU
|
||||
zMNZnzTDdTZl6nnui3RJtIYntSXR7ommC6ldY7nlnHnzkIEcDLwN6E/JNOB5gtTE
|
||||
L4ztUpKncHECgYBO3qHX6agasorjW52mZlh8UYxaEIMcurYwSzs+sATWJLX1/npz
|
||||
uhlMiOiZEMelduD9waD/Lf95u/HtCOrbopoL1DyhIlFTdkv0AooJXHX8Qz2JmPuQ
|
||||
WsZeJWcoawt1UumLtP//lkIEDEvO8/X3CIEhaxNYlQ7Yd//d+e67RZA5+wKBgD6f
|
||||
qR4m1iI4jPa7fw377wn3Wh7eOlx1Hziqvcv0CruUv004RPfDqxrn/k6A7/AGHWtE
|
||||
oTqyqY7oaa6jUvrhXBRJMd/nmBOaRXJJV/nF96R/s1hAP1UKE+xww5fSkhSqq0vm
|
||||
ZVWE7ihT/r9mFJAYzs3YA40MfjUPzPISpnKaFt2RAoGBANCtswMqztcuPDF5rL3d
|
||||
rqB6jwFrXKvwrx4HxOmF/MgGPyp6MWLBEnpZDvLJo9uSafq6Q6IwOQMWWF5GO7JO
|
||||
4EG9ldVugR/CtmL3+XTHE4MGPXmqHg/q/o7rItc7g11iXJTndcUZtWGwkHwl4zBF
|
||||
15NFZ2gU4rKnQ3sVAOzMoEw5
|
||||
-----END PRIVATE KEY-----
|
||||
"""
|
||||
|
||||
def _pad_b64_string(s):
|
||||
while len(s) % 4 is not 0:
|
||||
s = s + "="
|
||||
return s
|
||||
|
||||
|
||||
class TestKey(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.app = app.test_client()
|
||||
|
||||
def test_get_key(self):
|
||||
rv = self.app.get("/api/key")
|
||||
assert rv.status.startswith("200")
|
||||
data = json.loads(rv.data)
|
||||
assert data["keys"]
|
||||
|
||||
|
||||
class TestJWT(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.app = app.test_client()
|
||||
|
||||
def _assert_cookie_present(self, name):
|
||||
for cookie in self.app.cookie_jar:
|
||||
if cookie.name == name:
|
||||
return cookie.value
|
||||
raise AssertionError("Cookie named {} not found".format(name))
|
||||
|
||||
def _assert_cookie_not_present(self, name):
|
||||
for cookie in self.app.cookie_jar:
|
||||
assert cookie.name != name
|
||||
|
||||
def test_jwt_flow(self):
|
||||
"""
|
||||
First, try login attempts that are supposed to fail:
|
||||
1. checking login status without any prior interaction
|
||||
2. try logging in with an unknown user
|
||||
3. try logging in with a known user and bad password
|
||||
"""
|
||||
rv = self.app.get("/api/login")
|
||||
assert rv.status.startswith("401")
|
||||
self._assert_cookie_not_present("auth")
|
||||
|
||||
rv = self.app.post(
|
||||
"/api/login",
|
||||
data=json.dumps(
|
||||
{"username": "does@not.exist", "password": "somethingsomething"}
|
||||
),
|
||||
)
|
||||
assert rv.status.startswith("401")
|
||||
self._assert_cookie_not_present("auth")
|
||||
|
||||
rv = self.app.post(
|
||||
"/api/login",
|
||||
data=json.dumps({"username": "develop@offen.dev", "password": "developp"}),
|
||||
)
|
||||
assert rv.status.startswith("401")
|
||||
self._assert_cookie_not_present("auth")
|
||||
|
||||
"""
|
||||
Next, perform a successful login
|
||||
"""
|
||||
rv = self.app.post(
|
||||
"/api/login",
|
||||
data=json.dumps({"username": "develop@offen.dev", "password": "develop"}),
|
||||
)
|
||||
assert rv.status.startswith("200")
|
||||
|
||||
"""
|
||||
The response should contain information about the
|
||||
user and full information (i.e. a name) about the associated accounts
|
||||
"""
|
||||
data = json.loads(rv.data)
|
||||
assert data["user"]["userId"] is not None
|
||||
data["user"]["accounts"].sort(key=lambda a: a["name"])
|
||||
self.assertListEqual(
|
||||
data["user"]["accounts"],
|
||||
[
|
||||
{"name": "One", "accountId": "9b63c4d8-65c0-438c-9d30-cc4b01173393"},
|
||||
{"name": "Two", "accountId": "78403940-ae4f-4aff-a395-1e90f145cf62"},
|
||||
],
|
||||
)
|
||||
|
||||
"""
|
||||
The claims part of the JWT is expected to contain a valid expiry,
|
||||
information about the user and the associated account ids.
|
||||
"""
|
||||
jwt = self._assert_cookie_present("auth")
|
||||
# PyJWT strips the padding from the base64 encoded parts which Python
|
||||
# cannot decode properly, so we need to add the padding ourselves
|
||||
claims_part = _pad_b64_string(jwt.split(".")[1])
|
||||
claims = json.loads(base64.b64decode(claims_part))
|
||||
assert claims.get("exp") > time()
|
||||
|
||||
priv = claims.get("priv")
|
||||
assert priv is not None
|
||||
|
||||
assert priv.get("userId") is not None
|
||||
self.assertListEqual(
|
||||
priv["accounts"],
|
||||
[
|
||||
"9b63c4d8-65c0-438c-9d30-cc4b01173393",
|
||||
"78403940-ae4f-4aff-a395-1e90f145cf62",
|
||||
],
|
||||
)
|
||||
|
||||
"""
|
||||
Checking the login status when re-using the cookie should yield
|
||||
a successful response
|
||||
"""
|
||||
rv = self.app.get("/api/login")
|
||||
assert rv.status.startswith("200")
|
||||
jwt2 = self._assert_cookie_present("auth")
|
||||
assert jwt2 == jwt
|
||||
|
||||
"""
|
||||
Performing a bad login attempt when sending a valid auth cookie
|
||||
is expected to destroy the cookie and leave the user logged out again
|
||||
"""
|
||||
rv = self.app.post(
|
||||
"/api/login",
|
||||
data=json.dumps(
|
||||
{"username": "evil@session.takeover", "password": "develop"}
|
||||
),
|
||||
)
|
||||
assert rv.status.startswith("401")
|
||||
self._assert_cookie_not_present("auth")
|
||||
|
||||
"""
|
||||
Explicitly logging out leaves the user without cookies
|
||||
"""
|
||||
rv = self.app.post(
|
||||
"/api/login",
|
||||
data=json.dumps({"username": "develop@offen.dev", "password": "develop"}),
|
||||
)
|
||||
assert rv.status.startswith("200")
|
||||
|
||||
rv = self.app.post("/api/logout")
|
||||
assert rv.status.startswith("204")
|
||||
self._assert_cookie_not_present("auth")
|
||||
|
||||
def test_forged_token(self):
|
||||
"""
|
||||
The application needs to verify that tokens that would be theoretically
|
||||
valid are not signed using an unknown key.
|
||||
"""
|
||||
forged_token = jwt.encode(
|
||||
{
|
||||
"exp": datetime.utcnow() + timedelta(hours=24),
|
||||
"priv": {
|
||||
"userId": "8bc8db1b-f32d-4376-a1cf-724bf6a597b8",
|
||||
"accounts": [
|
||||
"9b63c4d8-65c0-438c-9d30-cc4b01173393",
|
||||
"78403940-ae4f-4aff-a395-1e90f145cf62",
|
||||
],
|
||||
},
|
||||
},
|
||||
FOREIGN_PRIVATE_KEY,
|
||||
algorithm="RS256",
|
||||
).decode()
|
||||
|
||||
self.app.set_cookie("localhost", "auth", forged_token)
|
||||
rv = self.app.get("/api/login")
|
||||
assert rv.status.startswith("401")
|
@ -31,7 +31,6 @@ services:
|
||||
POSTGRES_CONNECTION_STRING: postgres://postgres:develop@server_database:5432/postgres?sslmode=disable
|
||||
KMS_ENCRYPTION_ENDPOINT: http://kms:8081/encrypt
|
||||
PORT: 8080
|
||||
JWT_PUBLIC_KEY: http://accounts:5000/api/key
|
||||
DEVELOPMENT: '1'
|
||||
COOKIE_EXCHANGE_SECRET: Wsttdo4Z3mXV5sTc
|
||||
EVENT_RETENTION_PERIOD: 4464h
|
||||
@ -59,7 +58,6 @@ services:
|
||||
- KMS_HOST=http://localhost:8081
|
||||
- SCRIPT_HOST=http://localhost:9977
|
||||
- AUDITORIUM_HOST=http://localhost:8080
|
||||
- ACCOUNTS_HOST=http://localhost:5000
|
||||
- HOMEPAGE_HOST=http://localhost:8000
|
||||
|
||||
script:
|
||||
|
@ -14,12 +14,9 @@
|
||||
"homepage": "https://github.com/offen/offen#readme",
|
||||
"dependencies": {
|
||||
"serverless": "^1.45.0",
|
||||
"serverless-apigw-binary": "^0.4.4",
|
||||
"serverless-domain-manager": "^2.6.13",
|
||||
"serverless-finch": "^2.4.2",
|
||||
"serverless-pseudo-parameters": "^2.4.0",
|
||||
"serverless-python-requirements": "^4.3.0",
|
||||
"serverless-wsgi": "^1.7.2"
|
||||
"serverless-pseudo-parameters": "^2.4.0"
|
||||
},
|
||||
"devDependencies": {}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user