Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(auth): add subscription items endpoint #1478

Merged
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
d3809f2
feat(auth): add subscription items endpoint
oddgrd Dec 8, 2023
2820a16
feat(auth): move subscription item model to common
oddgrd Dec 11, 2023
05574fa
refactor(common): move stripe models to backend
oddgrd Dec 11, 2023
c90bc91
feat(auth): guard sub items endpoint with jwt layer
oddgrd Dec 12, 2023
2a24abb
wip(provisioner): update subscription on provisioning rds
oddgrd Dec 12, 2023
7ea8662
Merge branch 'main' into feature/eng-1946-increase-subscription-on-rd…
oddgrd Dec 13, 2023
950433d
revert(auth): fromrow changes, no longer needed with pg
oddgrd Dec 13, 2023
c4b32d0
docs(auth): add steps for running binary locally
oddgrd Dec 13, 2023
4340985
wip(deployer): send message on subscription increase
oddgrd Dec 13, 2023
f1ceb4d
fix(auth): invalid postgres query
oddgrd Dec 13, 2023
c478b66
fix(deployer): missing field in test insert query
oddgrd Dec 13, 2023
d2971de
fix(cargo-shuttle): display message on deploy and status
oddgrd Dec 13, 2023
a83666e
fix(auth): remove unused endpoint
oddgrd Dec 13, 2023
b9cbd62
refactor(auth): pass stripe client to sub update method
oddgrd Dec 13, 2023
18d9730
refactor(deployer): make cost increase msg generic
oddgrd Dec 14, 2023
c352e7b
refactor: simplify subscription dto, move price_id to auth
oddgrd Dec 14, 2023
a5ddd1b
feat: error handling, tests, comment deploy filter
oddgrd Dec 14, 2023
3a3c287
fix: clippy
oddgrd Dec 14, 2023
d38a53f
fix(provisioner): extraneous
oddgrd Dec 14, 2023
0028291
fix(provisioner): send item as json
oddgrd Dec 14, 2023
e78d42b
tests(provisioner): mock auth server
oddgrd Dec 14, 2023
e5e653e
fix(auth): sync_tier returns false if it didn't sync
oddgrd Dec 14, 2023
a8793c8
fix(auth): test returns 400 due to missing sub id
oddgrd Dec 14, 2023
130ee9d
misc: cleanup, instrumentation
oddgrd Dec 14, 2023
099f59d
Merge branch 'main' into feature/eng-1946-increase-subscription-on-rd…
oddgrd Dec 15, 2023
4a500ae
refactor: improve provisioner auth client error handling
oddgrd Dec 15, 2023
e508559
refactor(provisioner): retry suggestion in expired jwt error
oddgrd Dec 15, 2023
1dc1fd3
refactor(provisioner): remove unused unsafe impl
oddgrd Dec 15, 2023
8eb24d7
tests(auth): refactor stripe tests to wiremock
oddgrd Dec 18, 2023
6499db2
tests(auth): remove duplicate test
oddgrd Dec 18, 2023
574a486
misc(auth): cleanup tests
oddgrd Dec 18, 2023
4d2b659
misc: remove todos, guard against account downgrade
oddgrd Dec 18, 2023
44381de
refactor(auth): make price_id fetching env dependent
oddgrd Dec 18, 2023
0a081da
feat(auth): pass price_id as arg
oddgrd Dec 18, 2023
76ddf5b
feat(auth): add price id arg to makefile, dockerfile
oddgrd Dec 18, 2023
9bbd36e
docs(auth): add price id arg to readme guide
oddgrd Dec 18, 2023
6835532
feat(auth): add scopedlayer to update sub endpoint
oddgrd Dec 19, 2023
4317ee6
ci: add stripe-rds-price-id arg
oddgrd Dec 19, 2023
39f0897
misc(auth): remove commented debug_handler
oddgrd Dec 19, 2023
2029032
refactor(auth): sync and check tier in handler
oddgrd Dec 19, 2023
89f8b3b
refactor(auth): remove user add_sub_items method, do all checks in ha…
oddgrd Dec 19, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -834,9 +834,9 @@ workflows:
only: main
- approve-build-and-push-images-unstable:
type: approval
filters:
branches:
only: main
# filters:
# branches:
# only: main
Comment on lines +841 to +843
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reminder: to be removed before merging.

- build-and-push:
name: build-and-push-unstable
aws-access-key-id: DEV_AWS_ACCESS_KEY_ID
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?=""
oddgrd marked this conversation as resolved.
Show resolved Hide resolved

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.

36 changes: 36 additions & 0 deletions auth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# 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/pos
tgres start --stripe-secret-key sk_test_<test key> --jwt-signing-private-key <key>
```
oddgrd marked this conversation as resolved.
Show resolved Hide resolved
51 changes: 32 additions & 19 deletions auth/src/api/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@ 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,
metrics::{Metrics, TraceLayer},
},
request_span,
};
use sqlx::PgPool;
Expand All @@ -22,8 +26,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 +37,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 +59,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 +77,15 @@ 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(JwtAuthenticationLayer::new(move || {
let public_key = public_key.clone();
async move { public_key.clone() }
oddgrd marked this conversation as resolved.
Show resolved Hide resolved
})),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed only on this endpoint?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The auth API has only dealt with API keys or cookies so far, but rather than creating an admin user for provisioner and using that api key to call auth, we decided to just pass on the JWT of the user. This will be the only endpoint that needs to take a JWT, so we're just putting the layer on this handler for now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So what happens if there is no cached JWT and the first request made is to this endpoint?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, what do you mean by that? The provisioner needs a JWT to provision RDS, and it then uses this JWT to call the subscription update. It could be problematic if the JWT expires while provisioning the RDS instance, though, but in that case (which should be rare) we may just delete the RDS instance and the user needs to try again.

),
)
.route(
"/users/:account_name/:account_tier",
post(post_user).put(update_user_tier),
Expand All @@ -96,7 +109,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 +138,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