Skip to content

Commit

Permalink
feat: add subscription items endpoint and call it when provisioning r…
Browse files Browse the repository at this point in the history
…ds (#1478)
  • Loading branch information
oddgrd authored Dec 20, 2023
1 parent 402e3f0 commit 657815d
Show file tree
Hide file tree
Showing 34 changed files with 718 additions and 247 deletions.
12 changes: 9 additions & 3 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,9 @@ jobs:
stripe-secret-key:
description: "Stripe secret key used to connect a client to Stripe backend"
type: string
stripe-rds-price-id:
description: "Stripe price id of Shuttle AWS RDS product."
type: string
jwt-signing-private-key:
description: "Auth private key used for JWT signing"
type: string
Expand Down Expand Up @@ -457,6 +460,7 @@ jobs:
DEPLOYS_API_KEY=${<< parameters.deploys-api-key >>} \
LOGGER_POSTGRES_URI=${<< parameters.logger-postgres-uri >>} \
STRIPE_SECRET_KEY=${<< parameters.stripe-secret-key >>} \
STRIPE_RDS_PRICE_ID=${<< parameters.stripe-rds-price-id >>} \
AUTH_JWTSIGNING_PRIVATE_KEY=${<< parameters.jwt-signing-private-key >>} \
CONTROL_DB_POSTGRES_URI=${<< parameters.control-db-postgres-uri >>} \
make deploy
Expand Down Expand Up @@ -834,9 +838,9 @@ workflows:
only: main
- approve-build-and-push-images-unstable:
type: approval
filters:
branches:
only: main
# filters:
# branches:
# only: main
- build-and-push:
name: build-and-push-unstable
aws-access-key-id: DEV_AWS_ACCESS_KEY_ID
Expand All @@ -852,6 +856,7 @@ workflows:
deploys-api-key: DEV_DEPLOYS_API_KEY
logger-postgres-uri: DEV_LOGGER_POSTGRES_URI
stripe-secret-key: DEV_STRIPE_SECRET_KEY
stripe-rds-price-id: DEV_STRIPE_RDS_PRICE_ID
jwt-signing-private-key: DEV_AUTH_JWTSIGNING_PRIVATE_KEY
control-db-postgres-uri: DEV_CONTROL_DB_POSTGRES_URI
requires:
Expand Down Expand Up @@ -933,6 +938,7 @@ workflows:
deploys-api-key: PROD_DEPLOYS_API_KEY
logger-postgres-uri: PROD_LOGGER_POSTGRES_URI
stripe-secret-key: PROD_STRIPE_SECRET_KEY
stripe-rds-price-id: PROD_STRIPE_RDS_PRICE_ID
jwt-signing-private-key: PROD_AUTH_JWTSIGNING_PRIVATE_KEY
control-db-postgres-uri: PROD_CONTROL_DB_POSTGRES_URI
ssh-fingerprint: 6a:c5:33:fe:5b:c9:06:df:99:64:ca:17:0d:32:18:2e
Expand Down
66 changes: 66 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ MONGO_INITDB_ROOT_USERNAME?=mongodb
MONGO_INITDB_ROOT_PASSWORD?=password
STRIPE_SECRET_KEY?=""
AUTH_JWTSIGNING_PRIVATE_KEY?=""
STRIPE_RDS_PRICE_ID?=""

DD_ENV=$(SHUTTLE_ENV)
ifeq ($(SHUTTLE_ENV),production)
Expand Down Expand Up @@ -136,6 +137,7 @@ DOCKER_COMPOSE_ENV=\
MONGO_INITDB_ROOT_USERNAME=$(MONGO_INITDB_ROOT_USERNAME)\
MONGO_INITDB_ROOT_PASSWORD=$(MONGO_INITDB_ROOT_PASSWORD)\
STRIPE_SECRET_KEY=$(STRIPE_SECRET_KEY)\
STRIPE_RDS_PRICE_ID=$(STRIPE_RDS_PRICE_ID)\
AUTH_JWTSIGNING_PRIVATE_KEY=$(AUTH_JWTSIGNING_PRIVATE_KEY)\
DD_ENV=$(DD_ENV)\
USE_TLS=$(USE_TLS)\
Expand Down
2 changes: 2 additions & 0 deletions auth/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ pem = "2"
rand = { workspace = true }
ring = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
sqlx = { workspace = true, features = ["postgres", "json", "migrate"] }
strum = { workspace = true }
thiserror = { workspace = true }
Expand All @@ -43,3 +44,4 @@ portpicker = { workspace = true }
serde_json = { workspace = true }
shuttle-common-tests = { workspace = true }
tower = { workspace = true, features = ["util"] }
wiremock = "0.5"
13 changes: 0 additions & 13 deletions auth/README

This file was deleted.

40 changes: 40 additions & 0 deletions auth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Auth service considerations

## JWT signing private key

Starting the service locally requires provisioning of a base64 encoded PEM encoded PKCS#8 v1 unencrypted private key.
The service was tested with keys generated as follows:

```bash
openssl genpkey -algorithm ED25519 -out auth_jwtsigning_private_key.pem
base64 < auth_jwtsigning_private_key.pem
```

Used `OpenSSL 3.1.2 1 Aug 2023 (Library: OpenSSL 3.1.2 1 Aug 2023)` and `FreeBSD base64`, on a `macOS Sonoma 14.1.1`.

## Running the binary on it's own

**The below commands are ran from the root of the repo**

- First, start the control db container:

```
docker compose -f docker-compose.rendered.yml up control-db
```

- Then insert an admin user into the database:

```
cargo run --bin shuttle-auth -- --db-connection-uri postgres://postgres:postgres@localhost:5434/postgres init-admin --name admin
```

- Then start the service, you can get a stripe-secret-key from the Stripe dashboard. **Always use the test Stripe API for this**. See instructions above for generating a jwt-signing-private-key.

```
cargo run --bin shuttle-auth -- \
--db-connection-uri postgres://postgres:postgres@localhost:5434/postgres \
start \
--stripe-secret-key sk_test_<test key> \
--jwt-signing-private-key <key> \
--stripe-rds-price-id price_1OIS06FrN7EDaGOjaV0GXD7P
```
54 changes: 35 additions & 19 deletions auth/src/api/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,19 @@ use std::{net::SocketAddr, sync::Arc};

use axum::{
extract::FromRef,
handler::Handler,
middleware::from_extractor,
routing::{get, post, put},
Router, Server,
};
use axum_sessions::{async_session::MemoryStore, SessionLayer};
use rand::RngCore;
use shuttle_common::{
backends::metrics::{Metrics, TraceLayer},
backends::{
auth::{JwtAuthenticationLayer, ScopedLayer},
metrics::{Metrics, TraceLayer},
},
claims::Scope,
request_span,
};
use sqlx::PgPool;
Expand All @@ -22,8 +27,8 @@ use crate::{
};

use super::handlers::{
convert_cookie, convert_key, get_public_key, get_user, health_check, logout, post_user,
put_user_reset_key, refresh_token, update_user_tier,
add_subscription_items, convert_cookie, convert_key, get_public_key, get_user, health_check,
logout, post_user, put_user_reset_key, refresh_token, update_user_tier,
};

pub type UserManagerState = Arc<Box<dyn UserManagement>>;
Expand All @@ -33,6 +38,7 @@ pub type KeyManagerState = Arc<Box<dyn KeyManager>>;
pub struct RouterState {
pub user_manager: UserManagerState,
pub key_manager: KeyManagerState,
pub rds_price_id: String,
}

// Allow getting a user management state directly
Expand All @@ -54,17 +60,16 @@ pub struct ApiBuilder {
pool: Option<PgPool>,
session_layer: Option<SessionLayer<MemoryStore>>,
stripe_client: Option<stripe::Client>,
jwt_signing_private_key: Option<String>,
}

impl Default for ApiBuilder {
fn default() -> Self {
Self::new()
}
rds_price_id: Option<String>,
key_manager: EdDsaManager,
}

impl ApiBuilder {
pub fn new() -> Self {
pub fn new(jwt_signing_private_key: String) -> Self {
let key_manager = EdDsaManager::new(jwt_signing_private_key);

let public_key = key_manager.public_key().to_vec();

let router = Router::new()
.route("/", get(health_check))
.route("/logout", post(logout))
Expand All @@ -73,6 +78,17 @@ impl ApiBuilder {
.route("/auth/refresh", post(refresh_token))
.route("/public-key", get(get_public_key))
.route("/users/:account_name", get(get_user))
.route(
"/users/subscription/items",
post(
add_subscription_items
.layer(ScopedLayer::new(vec![Scope::ResourcesWrite]))
.layer(JwtAuthenticationLayer::new(move || {
let public_key = public_key.clone();
async move { public_key.clone() }
})),
),
)
.route(
"/users/:account_name/:account_tier",
post(post_user).put(update_user_tier),
Expand All @@ -96,7 +112,8 @@ impl ApiBuilder {
pool: None,
session_layer: None,
stripe_client: None,
jwt_signing_private_key: None,
rds_price_id: None,
key_manager,
}
}

Expand Down Expand Up @@ -124,27 +141,26 @@ impl ApiBuilder {
self
}

pub fn with_jwt_signing_private_key(mut self, private_key: String) -> Self {
self.jwt_signing_private_key = Some(private_key);
pub fn with_rds_price_id(mut self, price_id: String) -> Self {
self.rds_price_id = Some(price_id);
self
}

pub fn into_router(self) -> Router {
let pool = self.pool.expect("an sqlite pool is required");
let session_layer = self.session_layer.expect("a session layer is required");
let stripe_client = self.stripe_client.expect("a stripe client is required");
let jwt_signing_private_key = self
.jwt_signing_private_key
.expect("a jwt signing private key");
let rds_price_id = self.rds_price_id.expect("rds price id is required");

let user_manager = UserManager {
pool,
stripe_client,
};
let key_manager = EdDsaManager::new(jwt_signing_private_key);

let state = RouterState {
user_manager: Arc::new(Box::new(user_manager)),
key_manager: Arc::new(Box::new(key_manager)),
key_manager: Arc::new(Box::new(self.key_manager)),
rds_price_id,
};

self.router.layer(session_layer).with_state(state)
Expand Down
Loading

0 comments on commit 657815d

Please sign in to comment.