diff --git a/.env.template b/.env.template index a4a53ad8..25323a33 100644 --- a/.env.template +++ b/.env.template @@ -1,5 +1,6 @@ ADS_PORT=7676 DISCOUNTS_PORT=2814 +AUTH_PORT=7578 POSTGRES_USER=postgres POSTGRES_PASSWORD=postgres DD_API_KEY= \ No newline at end of file diff --git a/.github/workflows/auth.yml b/.github/workflows/auth.yml new file mode 100644 index 00000000..0138a935 --- /dev/null +++ b/.github/workflows/auth.yml @@ -0,0 +1,50 @@ +name: Auth + +on: + push: + branches: [ main ] + paths: + - services/auth/** + workflow_dispatch: + branches: [ main ] + +defaults: + run: + working-directory: auth + +jobs: + + build: + + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: us-east-1 + + - name: Login to ECR + id: login-ecr + uses: docker/login-action@v1 + with: + registry: public.ecr.aws + username: ${{ secrets.AWS_ACCESS_KEY_ID }} + password: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + + - name: Build and push + uses: docker/build-push-action@v2 + with: + context: ./services/ads + platforms: linux/amd64 + push: true + tags: ${{ secrets.PUBLIC_ECR_REGISTRY }}/storedog/auth:latest + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 963e4f61..098dba34 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,6 +39,7 @@ jobs: ${{ secrets.PUBLIC_ECR_REGISTRY }}/storedog/backend ${{ secrets.PUBLIC_ECR_REGISTRY }}/storedog/discounts ${{ secrets.PUBLIC_ECR_REGISTRY }}/storedog/ads + ${{ secrets.PUBLIC_ECR_REGISTRY }}/storedog/auth ) for i in "${IMAGES[@]}" diff --git a/docker-compose.yml b/docker-compose.yml index 5b584fa4..3d199edd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -38,6 +38,8 @@ services: DB_HOST: postgres DB_PORT: 5432 DISABLE_SPRING: 1 + DD_APPSEC_ENABLED: 1 + networks: - storedog-net worker: @@ -57,6 +59,7 @@ services: DB_HOST: postgres DB_PORT: 5432 DISABLE_SPRING: 1 + DD_APPSEC_ENABLED: 1 networks: - storedog-net ads: @@ -73,6 +76,7 @@ services: - DD_LOGS_INJECTION=true - DD_TRACE_ANALYTICS_ENABLED=true - DD_PROFILING_ENABLED=true + - DD_APPSEC_ENABLED=true build: context: ./services/ads command: flask run --port=${ADS_PORT} --host=0.0.0.0 # If using any other port besides the default 9292, overriding the CMD is required @@ -96,6 +100,7 @@ services: - DD_LOGS_INJECTION=true - DD_TRACE_ANALYTICS_ENABLED=true - DD_PROFILING_ENABLED=true + - DD_APPSEC_ENABLED=true build: context: ./services/discounts command: flask run --port=${DISCOUNTS_PORT} --host=0.0.0.0 # If using any other port besides the default 8282, overriding the CMD is required @@ -106,6 +111,32 @@ services: - "22" networks: - storedog-net + auth: + depends_on: + - postgres + profiles: + - csrf + environment: + - FLASK_APP=auth.py + - FLASK_DEBUG=1 + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - POSTGRES_USER=${POSTGRES_USER} + - POSTGRES_HOST=postgres + - DD_SERVICE=storedog-auth + - DD_AGENT_HOST=dd-agent + - DD_LOGS_INJECTION=true + - DD_TRACE_ANALYTICS_ENABLED=true + - DD_PROFILING_ENABLED=true + - DD_APPSEC_ENABLED=true + build: + context: ./services/auth + command: gunicorn --bind 0.0.0.0:${AUTH_PORT} auth:app # If using any other port besides the default 8282, overriding the CMD is required + volumes: + - "./services/auth:/app" + ports: + - "${AUTH_PORT}:${AUTH_PORT}" + networks: + - storedog-net dd-agent: image: gcr.io/datadoghq/agent:latest environment: diff --git a/services/ads/requirements.txt b/services/ads/requirements.txt index 218ca646..81a12424 100644 --- a/services/ads/requirements.txt +++ b/services/ads/requirements.txt @@ -1,7 +1,7 @@ certifi==2020.11.8 chardet==3.0.4 click==7.1.2 -ddtrace==0.57.3 +ddtrace==1.4.1 Flask==1.1.2 Flask-Cors==3.0.10 Flask-SQLAlchemy==2.4.4 diff --git a/services/auth/.dockerignore b/services/auth/.dockerignore new file mode 100644 index 00000000..d96d4988 --- /dev/null +++ b/services/auth/.dockerignore @@ -0,0 +1,37 @@ +# Version control +.git + +# Compiled Python bytecode +**/__pycache__ +**/*.pyc +**/*.pyo + +# Compiled extensions +**/*.pyd +**/*.so + +# coverage.py +.coverage +.coverage.* +htmlcov + +# Cached files +.cache +.mypy_cache +.hypothesis +.pytest_cache + +# Virtualenvs and builds +build/ +dist/ +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Docker +Dockerfile* +.dockerignore diff --git a/services/auth/Dockerfile b/services/auth/Dockerfile new file mode 100644 index 00000000..86995e7d --- /dev/null +++ b/services/auth/Dockerfile @@ -0,0 +1,27 @@ +# syntax = docker/dockerfile:1.2 +# ^ This enables the new BuildKit stable syntax which can be +# run with the DOCKER_BUILDKIT=1 environment variable in your +# docker build command (see build.sh) +FROM python:3.9.6-slim-buster + +# Update, upgrade, and cleanup debian packages +RUN export DEBIAN_FRONTEND=noninteractive && \ + apt-get update && \ + apt-get upgrade --yes && \ + apt-get install --yes build-essential libpq-dev && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +# Copy over app +WORKDIR /app +COPY . . + +# Install dependencies via pip and avoid caching build artifacts +RUN pip install --no-cache-dir -r requirements.txt + +# Set default Flask app and development environment +ENV FLASK_APP=auth.py + +# Start the app using ddtrace so we have profiling and tracing +ENTRYPOINT ["ddtrace-run"] +CMD gunicorn --bind 0.0.0.0:7578 auth:app diff --git a/services/auth/auth.py b/services/auth/auth.py new file mode 100644 index 00000000..3dceb2b5 --- /dev/null +++ b/services/auth/auth.py @@ -0,0 +1,83 @@ +from flask import Response, abort, redirect, request, jsonify +from flask_login import ( + LoginManager, + UserMixin, + current_user, + login_required, + login_user, + logout_user, +) + +from bootstrap import create_app +from models import Users + +app = create_app() +app.config.update( + DEBUG=True, + SECRET_KEY="secret_sauce", + +) + +login_manager = LoginManager() +login_manager.init_app(app) + +# csrf = CSRFProtect() +# csrf.init_app(app) + + +class User(UserMixin): + ... + + +def get_user(user_id: int): + users = Users.query.all() + for user in users: + if int(user.id) == int(user_id): + return user + return None + + +@login_manager.user_loader +def user_loader(id: int): + user = get_user(id) + if user: + user_model = User() + user_model.id = user.id + return user_model + return None + + +@app.errorhandler(401) +def unauthorized(error): + return Response("Not authorized"), 401 + + +@app.route("/", methods=["POST"]) +def homepage(): + if request.method == "POST": + username = request.form.get("username") + password = request.form.get("password") + users = Users.query.all() + + for user in users: + if user.username == username and user.password == password: + user_model = User() + user_model.id = user.id + login_user(user_model) + return jsonify({'User': 'Logged In'}) + else: + return abort(401) + + if current_user.is_authenticated: + return jsonify({'User': 'Already Logged In'}) + + +@app.route("/logout") +@login_required +def logout(): + logout_user() + return jsonify({'User': 'Logged Out'}) + + +if __name__ == "__main__": + app.run(debug=True) diff --git a/services/auth/bootstrap.py b/services/auth/bootstrap.py new file mode 100644 index 00000000..d688b420 --- /dev/null +++ b/services/auth/bootstrap.py @@ -0,0 +1,32 @@ +from flask import Flask +from models import Users, db + +import os +DB_USERNAME = os.environ['POSTGRES_USER'] +DB_PASSWORD = os.environ['POSTGRES_PASSWORD'] +DB_HOST = os.environ['POSTGRES_HOST'] + + +def create_app(): + """Create a Flask application""" + app = Flask(__name__) + #app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db' + app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://' + DB_USERNAME + ':' + DB_PASSWORD + '@' + DB_HOST + '/' + DB_USERNAME + app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + + db.init_app(app) + initialize_database(app, db) + return app + + +def initialize_database(app, db): + """Drop and restore database in a consistent state""" + app.logger.info('Running DB Init') + with app.app_context(): + db.drop_all() + db.create_all() + first_user = Users('devin.ford@datadoghq.com', 'test123') + second_user = Users('fake@notrealemail.com', 'fake123') + db.session.add(first_user) + db.session.add(second_user) + db.session.commit() diff --git a/services/auth/build.sh b/services/auth/build.sh new file mode 100755 index 00000000..bba5cb59 --- /dev/null +++ b/services/auth/build.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# Set bash strict mode so we insta-fail on any errors +# -e: Exit immediately if a command has non-zero exit code, i.e. fails somehow. +# Otherwise bash is like a Python program that just swallows exceptions. +# -u: Exit with error message if code uses an undefined environment variable, +# instead of silently continuing with an empty string. +# -o pipefail: Like -e, except for piped commands. +set -euo pipefail + +# Enable Docker Buildkit +export DOCKER_BUILDKIT=1 + +# Build and tag image +docker image build --progress=plain --tag ddtraining/auth:latest . diff --git a/services/auth/models.py b/services/auth/models.py new file mode 100644 index 00000000..f82b9ff2 --- /dev/null +++ b/services/auth/models.py @@ -0,0 +1,20 @@ +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() + + +class Users(db.Model): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(128)) + password = db.Column(db.String(64)) + + def __init__(self, username, password): + self.username = username + self.password = password + + def serialize(self): + return { + 'id': self.id, + 'username': self.username, + 'password': self.password, + } diff --git a/services/auth/requirements.txt b/services/auth/requirements.txt new file mode 100644 index 00000000..c4b7fb78 --- /dev/null +++ b/services/auth/requirements.txt @@ -0,0 +1,24 @@ +certifi==2020.11.8 +chardet==3.0.4 +click==8.0 +ddtrace==1.5.2 +Flask==2.2.2 +Flask-Login==0.6.2 +Flask-WTF==1.0.1 +Flask-Cors==3.0.10 +Flask-SQLAlchemy==3.0.2 +idna==2.10 +intervaltree==3.1.0 +itsdangerous==2.0 +Jinja2==3.0 +MarkupSafe==2.1.1 +nose==1.3.7 +protobuf==3.14.0 +requests==2.25.1 +six==1.15.0 +sortedcontainers==2.3.0 +SQLAlchemy==1.4.42 +psycopg2-binary +tenacity==6.2.0 +urllib3==1.26.5 +gunicorn==20.1.0 \ No newline at end of file diff --git a/services/backend/Dockerfile b/services/backend/Dockerfile index 365d2dad..6e0927c8 100644 --- a/services/backend/Dockerfile +++ b/services/backend/Dockerfile @@ -27,6 +27,7 @@ ENV BUNDLE_GEMFILE=/app/Gemfile \ GEM_HOME=/bundle ENV PATH="${BUNDLE_BIN}:${PATH}" + COPY . . RUN bundle install diff --git a/services/discounts/requirements.txt b/services/discounts/requirements.txt index e328b0f2..d23ddef0 100644 --- a/services/discounts/requirements.txt +++ b/services/discounts/requirements.txt @@ -1,7 +1,7 @@ certifi==2020.12.5 chardet==4.0.0 click==7.1.2 -ddtrace==0.57.3 +ddtrace==1.4.1 Flask==1.1.2 Flask-Cors==3.0.10 Flask-SQLAlchemy==2.4.4