chore: enable access control by env (#394)

This commit is contained in:
Nathan.fooo 2024-03-18 19:34:44 +08:00 committed by GitHub
parent c5112cc761
commit d1c82b7811
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 70 additions and 72 deletions

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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=

View File

@ -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:

View File

@ -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?;

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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();