chore: enable access control by env (#394)
This commit is contained in:
parent
c5112cc761
commit
d1c82b7811
|
|
@ -54,7 +54,6 @@ jobs:
|
|||
run: |
|
||||
sed -i '/image: appflowyinc\/appflowy_cloud:/d' docker-compose.yml
|
||||
sed -i '/image: appflowyinc\/admin_frontend:/d' docker-compose.yml
|
||||
sed -i 's/FEATURES: "disable_access_control"/FEATURES: ""/g' docker-compose.yml
|
||||
cat docker-compose.yml
|
||||
|
||||
- name: Run Docker-Compose
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ jobs:
|
|||
labels: ${{ steps.meta.outputs.labels }}
|
||||
provenance: false
|
||||
build-args: |
|
||||
FEATURES=disable_access_control
|
||||
FEATURES=
|
||||
|
||||
- name: Logout from Docker Hub
|
||||
if: always()
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ secrecy = { version = "0.8", features = ["serde"] }
|
|||
rand = { version = "0.8", features = ["std_rng"] }
|
||||
anyhow = "1.0.79"
|
||||
thiserror = "1.0.56"
|
||||
reqwest = { workspace = true, default-features = false, features = ["json", "rustls-tls", "cookies"] }
|
||||
reqwest = { workspace = true, features = ["json", "rustls-tls", "cookies"] }
|
||||
unicode-segmentation = "1.10"
|
||||
lazy_static = "1.4.0"
|
||||
fancy-regex = "0.11.0"
|
||||
|
|
@ -182,8 +182,6 @@ collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev
|
|||
|
||||
[features]
|
||||
custom_env= []
|
||||
# This feature will be removed once the cpu spike issue is resolved
|
||||
disable_access_control = []
|
||||
ai_enable = []
|
||||
|
||||
# Comment the above and uncomment the below to use local version of collab by cloning the repo and placing it in libs folder
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
APPFLOWY_GOTRUE_BASE_URL=http://gotrue:9999
|
||||
## URL that connects to the postgres docker container
|
||||
APPFLOWY_DATABASE_URL=postgres://postgres:password@postgres:5432/postgres
|
||||
APPFLOWY_ACCESS_CONTROL=true
|
||||
|
||||
# admin frontend
|
||||
## URL that connects to redis docker container
|
||||
|
|
|
|||
3
dev.env
3
dev.env
|
|
@ -1,6 +1,7 @@
|
|||
# gotrue URL that the appflowy service will use to connect to gotrue
|
||||
APPFLOWY_GOTRUE_BASE_URL=http://localhost:9999
|
||||
APPFLOWY_DATABASE_URL=postgres://postgres:password@localhost:5432/postgres
|
||||
APPFLOWY_ACCESS_CONTROL=true
|
||||
|
||||
# This file is used to set the environment variables for local development
|
||||
# Copy this file to .env and change the values as needed
|
||||
|
|
@ -85,4 +86,4 @@ GF_SECURITY_ADMIN_PASSWORD=password
|
|||
CLOUDFLARE_TUNNEL_TOKEN=
|
||||
|
||||
# AppFlowy AI
|
||||
OPENAI_API_KEY=
|
||||
OPENAI_API_KEY=
|
||||
|
|
|
|||
|
|
@ -106,11 +106,12 @@ services:
|
|||
- APPFLOWY_S3_BUCKET=${APPFLOWY_S3_BUCKET}
|
||||
- APPFLOWY_S3_REGION=${APPFLOWY_S3_REGION}
|
||||
- APPFLOWY_AI_URL=${APPFLOWY_AI_URL}
|
||||
- APPFLOWY_ACCESS_CONTROL=${APPFLOWY_ACCESS_CONTROL}
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
FEATURES: "disable_access_control"
|
||||
FEATURES: ""
|
||||
image: appflowyinc/appflowy_cloud:${BACKEND_VERSION:-latest}
|
||||
|
||||
admin_frontend:
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use crate::api::file_storage::file_storage_scope;
|
|||
use crate::api::user::user_scope;
|
||||
use crate::api::workspace::{collab_scope, workspace_scope};
|
||||
use crate::api::ws::ws_scope;
|
||||
use crate::biz::casbin::access_control::AccessControl;
|
||||
use crate::biz::casbin::access_control::{enable_access_control, AccessControl};
|
||||
|
||||
use crate::biz::casbin::RealtimeCollabAccessControlImpl;
|
||||
use crate::biz::collab::access_control::{
|
||||
|
|
@ -164,9 +164,6 @@ fn get_certificate_and_server_key(config: &Config) -> Option<(Secret<String>, Se
|
|||
|
||||
pub async fn init_state(config: &Config, rt_cmd_tx: RTCommandSender) -> Result<AppState, Error> {
|
||||
// Print the feature flags
|
||||
if cfg!(feature = "disable_access_control") {
|
||||
info!("Access control is disabled");
|
||||
}
|
||||
|
||||
let metrics = AppMetrics::new();
|
||||
|
||||
|
|
@ -199,7 +196,10 @@ pub async fn init_state(config: &Config, rt_cmd_tx: RTCommandSender) -> Result<A
|
|||
let collab_member_listener = pg_listeners.subscribe_collab_member_change();
|
||||
let workspace_member_listener = pg_listeners.subscribe_workspace_member_change();
|
||||
|
||||
info!("Setting up access controls...");
|
||||
info!(
|
||||
"Setting up access controls, is_enable: {}",
|
||||
enable_access_control()
|
||||
);
|
||||
let access_control =
|
||||
AccessControl::new(pg_pool.clone(), metrics.access_control_metrics.clone()).await?;
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ use anyhow::anyhow;
|
|||
|
||||
use sqlx::PgPool;
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use redis::{ErrorKind, FromRedisValue, RedisError, RedisResult, RedisWrite, ToRedisArgs, Value};
|
||||
use std::sync::Arc;
|
||||
|
||||
|
|
@ -88,28 +89,28 @@ impl AccessControl {
|
|||
obj: &ObjectType<'_>,
|
||||
act: &ActionType,
|
||||
) -> Result<(), AppError> {
|
||||
if cfg!(feature = "disable_access_control") {
|
||||
Ok(())
|
||||
} else {
|
||||
if enable_access_control() {
|
||||
let result = self.enforcer.update_policy(uid, obj, act).await;
|
||||
let _ = self.change_tx.send(AccessControlChange::UpdatePolicy {
|
||||
uid: *uid,
|
||||
oid: obj.object_id().to_string(),
|
||||
});
|
||||
result
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn remove_policy(&self, uid: &i64, obj: &ObjectType<'_>) -> Result<(), AppError> {
|
||||
if cfg!(feature = "disable_access_control") {
|
||||
Ok(())
|
||||
} else {
|
||||
if enable_access_control() {
|
||||
self.enforcer.remove_policy(uid, obj).await?;
|
||||
let _ = self.change_tx.send(AccessControlChange::RemovePolicy {
|
||||
uid: *uid,
|
||||
oid: obj.object_id().to_string(),
|
||||
});
|
||||
Ok(())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -117,17 +118,17 @@ impl AccessControl {
|
|||
where
|
||||
A: ToACAction,
|
||||
{
|
||||
if cfg!(feature = "disable_access_control") {
|
||||
Ok(true)
|
||||
} else {
|
||||
if enable_access_control() {
|
||||
self.enforcer.enforce_policy(uid, obj, act).await
|
||||
} else {
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// policy in db:
|
||||
/// p = 1, 123, 1 (1 mean AFRole::Owner)
|
||||
/// p = 1, 456, 50 (50 mean AFAccessLevel::FullAccess)
|
||||
/// policy:
|
||||
/// p = sub=uid, obj=object_id, act=role_id
|
||||
/// p = sub=uid, obj=object_id, act=access_level
|
||||
///
|
||||
/// role_definition in db:
|
||||
/// g = _, _
|
||||
|
|
@ -354,3 +355,17 @@ impl FromACAction for AFRole {
|
|||
Self::from(action)
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref ENABLE_ACCESS_CONTROL: bool = {
|
||||
match std::env::var("APPFLOWY_ACCESS_CONTROL") {
|
||||
Ok(value) => value.eq_ignore_ascii_case("true") || value.eq("1"),
|
||||
Err(_) => false,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn enable_access_control() -> bool {
|
||||
*ENABLE_ACCESS_CONTROL
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
use crate::biz::casbin::access_control::{AccessControl, AccessControlChange, Action};
|
||||
use crate::biz::casbin::access_control::{
|
||||
enable_access_control, AccessControl, AccessControlChange, Action,
|
||||
};
|
||||
use crate::biz::casbin::access_control::{ActionType, ObjectType};
|
||||
use crate::biz::collab::access_control::CollabAccessControl;
|
||||
use app_error::AppError;
|
||||
|
|
@ -107,27 +109,28 @@ impl RealtimeCollabAccessControlImpl {
|
|||
oid: &str,
|
||||
required_action: Action,
|
||||
) -> Result<bool, AppError> {
|
||||
if cfg!(feature = "disable_access_control") {
|
||||
return Ok(true);
|
||||
}
|
||||
let key = cache_key(*uid, oid);
|
||||
// Check if the action is already cached
|
||||
if let Some(action) = self.action_by_oid.get(&key) {
|
||||
return Ok(*action >= required_action);
|
||||
}
|
||||
if enable_access_control() {
|
||||
let key = cache_key(*uid, oid);
|
||||
// Check if the action is already cached
|
||||
if let Some(action) = self.action_by_oid.get(&key) {
|
||||
return Ok(*action >= required_action);
|
||||
}
|
||||
|
||||
// Not in cache, enforce access control
|
||||
let is_permitted = self
|
||||
.access_control
|
||||
.enforce(uid, &ObjectType::Collab(oid), &required_action)
|
||||
.await?;
|
||||
// Not in cache, enforce access control
|
||||
let is_permitted = self
|
||||
.access_control
|
||||
.enforce(uid, &ObjectType::Collab(oid), &required_action)
|
||||
.await?;
|
||||
|
||||
if is_permitted {
|
||||
// Permission granted, cache the action
|
||||
self.action_by_oid.insert(key, required_action);
|
||||
if is_permitted {
|
||||
// Permission granted, cache the action
|
||||
self.action_by_oid.insert(key, required_action);
|
||||
}
|
||||
|
||||
Ok(is_permitted)
|
||||
} else {
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
Ok(is_permitted)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
31
src/main.rs
31
src/main.rs
|
|
@ -1,7 +1,6 @@
|
|||
use appflowy_cloud::application::{init_state, Application};
|
||||
use appflowy_cloud::config::config::get_configuration;
|
||||
use appflowy_cloud::telemetry::init_subscriber;
|
||||
use tracing::info;
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
|
|
@ -21,34 +20,8 @@ async fn main() -> anyhow::Result<()> {
|
|||
get_configuration().map_err(|e| anyhow::anyhow!("Failed to read configuration: {}", e))?;
|
||||
init_subscriber(&conf.app_env, filters);
|
||||
|
||||
// If current build is debug and the feature "custom_env" is not enabled, load from .env
|
||||
// otherwise, load from .env.without_nginx.
|
||||
if cfg!(debug_assertions) {
|
||||
#[cfg(not(feature = "custom_env"))]
|
||||
{
|
||||
info!("custom_env is disable, load from .env");
|
||||
dotenvy::dotenv().ok();
|
||||
}
|
||||
|
||||
#[cfg(feature = "custom_env")]
|
||||
{
|
||||
match dotenvy::from_filename(".env.without_nginx") {
|
||||
Ok(_) => {
|
||||
info!("custom_env is enabled, load from .env.without_nginx");
|
||||
},
|
||||
Err(err) => {
|
||||
tracing::error!(
|
||||
"Failed to load .env.without_nginx: {}, fallback to .env file",
|
||||
err
|
||||
);
|
||||
dotenvy::dotenv().ok();
|
||||
},
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// In release, always load from .env
|
||||
dotenvy::dotenv().ok();
|
||||
}
|
||||
// Load environment variables from .env file
|
||||
dotenvy::dotenv().ok();
|
||||
|
||||
let (tx, rx) = tokio::sync::mpsc::channel(1000);
|
||||
let state = init_state(&conf, tx)
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ use std::future::{ready, Ready};
|
|||
use std::sync::Arc;
|
||||
use tracing::error;
|
||||
|
||||
use crate::biz::casbin::access_control::enable_access_control;
|
||||
use crate::state::AppState;
|
||||
use app_error::AppError;
|
||||
use uuid::Uuid;
|
||||
|
|
@ -118,6 +119,12 @@ where
|
|||
forward_ready!(service);
|
||||
|
||||
fn call(&self, mut req: ServiceRequest) -> Self::Future {
|
||||
// If the access control is not enabled, skip the access control
|
||||
if !enable_access_control() {
|
||||
let fut = self.service.call(req);
|
||||
return Box::pin(fut);
|
||||
}
|
||||
|
||||
let path = req.match_pattern().map(|pattern| {
|
||||
// Create ResourceDef will cause memory leak, so we use the cache to store the ResourceDef
|
||||
let mut path = req.match_info().clone();
|
||||
|
|
|
|||
Loading…
Reference in New Issue