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: gateway command to sync permit #1705

Merged
merged 15 commits into from
Apr 2, 2024
6 changes: 6 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,9 @@ jobs:
gateway-admin-key:
description: "Admin API key that authorizes gateway requests to auth service, for key to jwt conversion."
type: string
permit-api-key:
description: "Permit.io API key for the Permit environment that matches the current ${SHUTTLE_ENV}."
type: string
steps:
- checkout
- set-git-tag
Expand Down Expand Up @@ -383,6 +386,7 @@ jobs:
AUTH_JWTSIGNING_PRIVATE_KEY=${<< parameters.jwt-signing-private-key >>} \
CONTROL_DB_POSTGRES_URI=${<< parameters.control-db-postgres-uri >>} \
GATEWAY_ADMIN_KEY=${<< parameters.gateway-admin-key >>} \
PERMIT_API_KEY=${<< parameters.permit-api-key >>} \
make deploy
- when:
condition:
Expand Down Expand Up @@ -748,6 +752,7 @@ workflows:
jwt-signing-private-key: DEV_AUTH_JWTSIGNING_PRIVATE_KEY
control-db-postgres-uri: DEV_CONTROL_DB_POSTGRES_URI
gateway-admin-key: DEV_GATEWAY_ADMIN_KEY
permit-api-key: STAGING_PERMIT_API_KEY
requires:
- build-and-push-unstable
- approve-deploy-images-unstable
Expand Down Expand Up @@ -832,6 +837,7 @@ workflows:
jwt-signing-private-key: PROD_AUTH_JWTSIGNING_PRIVATE_KEY
control-db-postgres-uri: PROD_CONTROL_DB_POSTGRES_URI
gateway-admin-key: PROD_GATEWAY_ADMIN_KEY
permit-api-key: PROD_PERMIT_API_KEY
ssh-fingerprint: 6a:c5:33:fe:5b:c9:06:df:99:64:ca:17:0d:32:18:2e
ssh-config-script: production-ssh-config.sh
ssh-host: shuttle.prod.internal
Expand Down
3 changes: 0 additions & 3 deletions Cargo.lock

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

17 changes: 10 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ STRIPE_SECRET_KEY?=""
AUTH_JWTSIGNING_PRIVATE_KEY?=""
PERMIT_API_KEY?=""

# log level set in all backends
RUST_LOG?=shuttle=debug,info

# production/staging/dev
SHUTTLE_ENV?=dev
DD_ENV=$(SHUTTLE_ENV)
ifeq ($(SHUTTLE_ENV),production)
DOCKER_COMPOSE_FILES=docker-compose.yml
Expand All @@ -53,8 +58,8 @@ CONTAINER_REGISTRY=public.ecr.aws/shuttle
# make sure we only ever go to production with `--tls=enable`
USE_TLS=enable
CARGO_PROFILE=release
RUST_LOG?=shuttle=debug,info
else
# add local development overrides to compose
DOCKER_COMPOSE_FILES=docker-compose.yml docker-compose.dev.yml
STACK?=shuttle-dev
APPS_FQDN=unstable.shuttleapp.rs
Expand All @@ -63,7 +68,10 @@ CONTAINER_REGISTRY=public.ecr.aws/shuttle-dev
USE_TLS?=disable
# default for local run
CARGO_PROFILE?=debug
RUST_LOG?=shuttle=debug,info
ifeq ($(CI),true)
# use release builds for staging deploys so that the DLC cache can be re-used for prod deploys
CARGO_PROFILE=release
endif
DEV_SUFFIX=-dev
DEPLOYS_API_KEY?=gateway4deployes
GATEWAY_ADMIN_KEY?=dh9z58jttoes3qvt
Expand All @@ -79,11 +87,6 @@ LOGGER_POSTGRES_PASSWORD?=postgres
LOGGER_POSTGRES_URI?=postgres://postgres:${LOGGER_POSTGRES_PASSWORD}@logger-postgres:5432/postgres
endif

ifeq ($(CI),true)
# default for staging
CARGO_PROFILE=release
endif

POSTGRES_EXTRA_PATH?=./extras/postgres
POSTGRES_TAG?=14

Expand Down
2 changes: 2 additions & 0 deletions auth/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ pub enum Error {
Stripe(#[from] StripeError),
#[error("Failed to communicate with service API.")]
ServiceApi(#[from] client::Error),
#[error("Failed to communicate with Permit API.")]
PermitApi(#[from] client::permit::Error),
}

impl Serialize for Error {
Expand Down
37 changes: 21 additions & 16 deletions auth/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ mod user;

use anyhow::Result;
use args::{CopyPermitEnvArgs, StartArgs, SyncArgs};
use shuttle_backends::client::{permit, PermissionsDal};
use http::StatusCode;
use shuttle_backends::client::{
permit::{self, Error, ResponseContent},
PermissionsDal,
};
use shuttle_common::{claims::AccountTier, ApiKey};
use sqlx::{migrate::Migrator, query, PgPool};
use tracing::info;
Expand Down Expand Up @@ -54,37 +58,38 @@ pub async fn sync(pool: PgPool, args: SyncArgs) -> Result<()> {
match permit_client.get_user(&user.id).await {
Ok(p_user) => {
// Update tier if out of sync
let wanted_tier = user.account_tier.as_permit_account_tier();
if !p_user
.roles
.is_some_and(|rs| rs.iter().any(|r| r.role == user.account_tier.to_string()))
.is_some_and(|rs| rs.iter().any(|r| r.role == wanted_tier.to_string()))
{
match user.account_tier {
AccountTier::Basic
| AccountTier::PendingPaymentPro
| AccountTier::CancelledPro
| AccountTier::Team
| AccountTier::Admin
| AccountTier::Deployer => {
println!("updating tier for user: {}", user.id);
match wanted_tier {
AccountTier::Basic => {
permit_client.make_basic(&user.id).await?;
}
AccountTier::Pro => {
permit_client.make_pro(&user.id).await?;
}
_ => unreachable!(),
}
}
}
Err(_) => {
// FIXME: Make the error type better so that this is only done on 404s

Err(Error::ResponseError(ResponseContent {
status: StatusCode::NOT_FOUND,
..
})) => {
// Add users that are not in permit
println!("creating user: {}", user.id);

// Add users that are not in permit
permit_client.new_user(&user.id).await?;

if user.account_tier == AccountTier::Pro {
if user.account_tier.as_permit_account_tier() == AccountTier::Pro {
jonaro00 marked this conversation as resolved.
Show resolved Hide resolved
permit_client.make_pro(&user.id).await?;
}
}
Err(e) => {
println!("failed to fetch user {}. skipping. error: {e}", user.id);
}
}
}

Expand All @@ -100,7 +105,7 @@ pub async fn copy_environment(args: CopyPermitEnvArgs) -> Result<()> {
args.permit.permit_api_key,
);

client.copy_environment(&args.target).await
Ok(client.copy_environment(&args.target).await?)
}

pub async fn init(pool: PgPool, args: InitArgs, tier: AccountTier) -> Result<()> {
Expand Down
68 changes: 65 additions & 3 deletions backends/src/client/permit.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
use anyhow::Error;
use std::fmt::{Debug, Display};

// use anyhow::Error;
jonaro00 marked this conversation as resolved.
Show resolved Hide resolved
use async_trait::async_trait;
use permit_client_rs::{
apis::{
resource_instances_api::{create_resource_instance, delete_resource_instance},
role_assignments_api::{assign_role, unassign_role},
users_api::{create_user, delete_user, get_user},
Error as PermitClientError,
},
models::{
ResourceInstanceCreate, RoleAssignmentCreate, RoleAssignmentRemove, UserCreate, UserRead,
Expand All @@ -17,6 +20,7 @@ use permit_pdp_client_rs::{
},
data_updater_api::trigger_policy_data_update_data_updater_trigger_post,
policy_updater_api::trigger_policy_update_policy_updater_trigger_post,
Error as PermitPDPClientError,
},
models::{AuthorizationQuery, Resource, User, UserPermissionsQuery, UserPermissionsResult},
};
Expand Down Expand Up @@ -143,6 +147,7 @@ impl PermissionsDal for Client {
}

async fn create_project(&self, user_id: &str, project_id: &str) -> Result<(), Error> {
// TODO?: Ignore error if 409?
jonaro00 marked this conversation as resolved.
Show resolved Hide resolved
create_resource_instance(
&self.api,
&self.proj_id,
Expand Down Expand Up @@ -492,7 +497,7 @@ impl Client {
}
}

// #[cfg(feature = "admin")]
/// Higher level management methods. Use with care.
mod admin {
use permit_client_rs::{
apis::environments_api::copy_environment,
Expand All @@ -505,7 +510,8 @@ mod admin {
use super::*;

impl Client {
/// Copy and overwrite the policies of one env to another existing one
/// Copy and overwrite a permit env's policies to another env.
/// Requires a project level API key.
pub async fn copy_environment(&self, target_env: &str) -> Result<(), Error> {
copy_environment(
&self.api,
Expand Down Expand Up @@ -543,3 +549,59 @@ mod admin {
}
}
}

/// Dumbed down and unified version of the client's errors to get rid of the genereic <T>
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("reqwest error: {0}")]
Reqwest(reqwest::Error),
#[error("serde error: {0}")]
Serde(serde_json::Error),
#[error("io error: {0}")]
Io(std::io::Error),
#[error("response error: {0}")]
ResponseError(ResponseContent),
}
#[derive(Debug)]
pub struct ResponseContent {
pub status: reqwest::StatusCode,
pub content: String,
pub entity: String,
}
impl Display for ResponseContent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"status: {}, content: {}, entity: {}",
self.status, self.content, self.entity
)
}
}
impl<T: Debug> From<PermitClientError<T>> for Error {
fn from(value: PermitClientError<T>) -> Self {
match value {
PermitClientError::Reqwest(e) => Self::Reqwest(e),
PermitClientError::Serde(e) => Self::Serde(e),
PermitClientError::Io(e) => Self::Io(e),
PermitClientError::ResponseError(e) => Self::ResponseError(ResponseContent {
status: e.status,
content: e.content,
entity: format!("{:?}", e.entity),
}),
}
}
}
impl<T: Debug> From<PermitPDPClientError<T>> for Error {
fn from(value: PermitPDPClientError<T>) -> Self {
match value {
PermitPDPClientError::Reqwest(e) => Self::Reqwest(e),
PermitPDPClientError::Serde(e) => Self::Serde(e),
PermitPDPClientError::Io(e) => Self::Io(e),
PermitPDPClientError::ResponseError(e) => Self::ResponseError(ResponseContent {
status: e.status,
content: e.content,
entity: format!("{:?}", e.entity),
}),
}
}
}
3 changes: 1 addition & 2 deletions backends/src/test_utils/gateway.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use std::sync::Arc;

use anyhow::Error;
use async_trait::async_trait;
use permit_client_rs::models::UserRead;
use permit_pdp_client_rs::models::UserPermissionsResult;
Expand All @@ -12,7 +11,7 @@ use wiremock::{
Mock, MockServer, Request, ResponseTemplate,
};

use crate::client::PermissionsDal;
use crate::client::{permit::Error, PermissionsDal};

pub async fn get_mocked_gateway_server() -> MockServer {
let mock_server = MockServer::start().await;
Expand Down
14 changes: 12 additions & 2 deletions backends/tests/integration/permit_tests.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
mod needs_docker {
use std::sync::OnceLock;

use http::StatusCode;
use permit_client_rs::apis::{
resource_instances_api::{delete_resource_instance, list_resource_instances},
users_api::list_users,
};
use serial_test::serial;
use shuttle_backends::client::{permit::Client, PermissionsDal};
use shuttle_backends::client::{
permit::{Client, Error, ResponseContent},
PermissionsDal,
};
use shuttle_common::claims::AccountTier;
use shuttle_common_tests::permit_pdp::DockerInstance;
use test_context::{test_context, AsyncTestContext};
Expand Down Expand Up @@ -116,7 +120,13 @@ mod needs_docker {
client.delete_user(u).await.unwrap();
let res = client.get_user(u).await;

assert!(res.is_err());
assert!(matches!(
res,
Err(Error::ResponseError(ResponseContent {
status: StatusCode::NOT_FOUND,
..
}))
));
}

#[test_context(Wrap)]
Expand Down
19 changes: 18 additions & 1 deletion common/src/claims.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,9 +169,9 @@ impl ScopeBuilder {
)]
#[serde(rename_all = "lowercase")]
#[cfg_attr(feature = "display", derive(strum::Display))]
#[cfg_attr(feature = "display", strum(serialize_all = "lowercase"))]
#[cfg_attr(feature = "persist", derive(sqlx::Type))]
#[cfg_attr(feature = "persist", sqlx(rename_all = "lowercase"))]
#[cfg_attr(feature = "display", strum(serialize_all = "lowercase"))]
pub enum AccountTier {
#[default]
Basic,
Expand All @@ -184,6 +184,23 @@ pub enum AccountTier {
Deployer,
}

impl AccountTier {
/// The tier that this user should have in Permit.io.
/// Permit should only store the tier that determines permissions,
/// with the exception of 'admin', which is an override and not checked against Permit.
pub fn as_permit_account_tier(&self) -> Self {
match self {
Self::Basic
| Self::PendingPaymentPro
| Self::CancelledPro
| Self::Team
| Self::Admin
| Self::Deployer => Self::Basic,
Self::Pro => Self::Pro,
}
}
}

impl From<AccountTier> for Vec<Scope> {
fn from(tier: AccountTier) -> Self {
let mut builder = ScopeBuilder::new();
Expand Down
6 changes: 6 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -290,3 +290,9 @@ services:
placement:
constraints:
- node.hostname==controller
healthcheck:
test: curl -f -s http://localhost:7000
interval: 1m
timeout: 10s
retries: 5

2 changes: 1 addition & 1 deletion gateway/src/api/latest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -768,7 +768,7 @@ async fn renew_gateway_acme_certificate(
.whole_days()
<= RENEWAL_VALIDITY_THRESHOLD_IN_DAYS
{
let tls_path = service.state_location.join("ssl.pem");
let tls_path = service.state_dir.join("ssl.pem");
let certs = service
.create_certificate(&acme_client, account.credentials())
.await;
Expand Down
Loading