1000f8a80d
- Add gRPC service modules: auth, channel, channel settings, member, permission - Update protobuf definitions and generated code - Remove immediate/ real-time module (superseded by IM service) - Update etcd discovery and registration - Update cache, error, config, and build infrastructure - Add ADR documentation - Update OpenAPI spec
361 lines
12 KiB
Rust
361 lines
12 KiB
Rust
use tonic::{Request, Response, Status};
|
|
use uuid::Uuid;
|
|
|
|
use crate::models::channels::ChannelPermissionOverwrite;
|
|
use crate::models::common::{OverwriteTarget, Role};
|
|
use crate::pb::im::permission_service_server::PermissionService;
|
|
use crate::pb::im::{
|
|
CheckPermissionRequest, CheckPermissionResponse, DeletePermissionOverwriteRequest,
|
|
DeletePermissionOverwriteResponse, EnsureReadableRequest, EnsureReadableResponse,
|
|
GetPermissionOverwritesRequest, GetPermissionOverwritesResponse, GetPermissionsRequest,
|
|
GetPermissionsResponse, PermissionOverwrite, ResolveChannelRequest, ResolveChannelResponse,
|
|
SetPermissionOverwriteRequest, SetPermissionOverwriteResponse,
|
|
};
|
|
use crate::service::util::role_level;
|
|
use crate::service::AppService;
|
|
|
|
pub struct PermissionGrpcService {
|
|
service: AppService,
|
|
}
|
|
|
|
impl PermissionGrpcService {
|
|
pub fn new(service: AppService) -> Self {
|
|
Self { service }
|
|
}
|
|
|
|
fn parse_uuid(s: &str, field: &str) -> Result<Uuid, Status> {
|
|
Uuid::parse_str(s).map_err(|e| Status::invalid_argument(format!("{field}: {e}")))
|
|
}
|
|
|
|
fn im_permission_to_str(v: i32) -> &'static str {
|
|
match v {
|
|
1 => "READ_CHANNEL",
|
|
2 => "SEND_MESSAGE",
|
|
3 => "MANAGE_THREADS",
|
|
4 => "MANAGE_REACTIONS",
|
|
5 => "MANAGE_PINS",
|
|
6 => "INVITE_MEMBERS",
|
|
7 => "KICK_MEMBERS",
|
|
8 => "MANAGE_CHANNEL",
|
|
9 => "MANAGE_ROLES",
|
|
10 => "MANAGE_WEBHOOKS",
|
|
11 => "MANAGE_EMOJIS",
|
|
12 => "VIEW_AUDIT_LOG",
|
|
13 => "MANAGE_INTEGRATIONS",
|
|
14 => "SEND_TTS",
|
|
15 => "USE_SLASH_COMMANDS",
|
|
16 => "ATTACH_FILES",
|
|
17 => "MENTION_EVERYONE",
|
|
18 => "MANAGE_MESSAGES",
|
|
19 => "ADMIN",
|
|
_ => "UNSPECIFIED",
|
|
}
|
|
}
|
|
|
|
fn str_to_im_permission(s: &str) -> i32 {
|
|
match s {
|
|
"READ_CHANNEL" => 1,
|
|
"SEND_MESSAGE" => 2,
|
|
"MANAGE_THREADS" => 3,
|
|
"MANAGE_REACTIONS" => 4,
|
|
"MANAGE_PINS" => 5,
|
|
"INVITE_MEMBERS" => 6,
|
|
"KICK_MEMBERS" => 7,
|
|
"MANAGE_CHANNEL" => 8,
|
|
"MANAGE_ROLES" => 9,
|
|
"MANAGE_WEBHOOKS" => 10,
|
|
"MANAGE_EMOJIS" => 11,
|
|
"VIEW_AUDIT_LOG" => 12,
|
|
"MANAGE_INTEGRATIONS" => 13,
|
|
"SEND_TTS" => 14,
|
|
"USE_SLASH_COMMANDS" => 15,
|
|
"ATTACH_FILES" => 16,
|
|
"MENTION_EVERYONE" => 17,
|
|
"MANAGE_MESSAGES" => 18,
|
|
"ADMIN" => 19,
|
|
_ => 0,
|
|
}
|
|
}
|
|
|
|
fn permission_requires_role(p: i32) -> Role {
|
|
match p {
|
|
1 => Role::Viewer,
|
|
2 => Role::Member,
|
|
3 => Role::Member,
|
|
4 => Role::Member,
|
|
5 => Role::Moderator,
|
|
6 => Role::Moderator,
|
|
7 => Role::Moderator,
|
|
8 => Role::Admin,
|
|
9 => Role::Admin,
|
|
10 => Role::Admin,
|
|
11 => Role::Admin,
|
|
12 => Role::Moderator,
|
|
13 => Role::Admin,
|
|
14 => Role::Member,
|
|
15 => Role::Member,
|
|
16 => Role::Member,
|
|
17 => Role::Moderator,
|
|
18 => Role::Moderator,
|
|
19 => Role::Admin,
|
|
_ => Role::Owner,
|
|
}
|
|
}
|
|
|
|
fn role_to_permissions(role: Role) -> Vec<i32> {
|
|
let level = role_level(role);
|
|
let mut perms = Vec::new();
|
|
|
|
if level >= role_level(Role::Viewer) {
|
|
perms.push(1);
|
|
}
|
|
if level >= role_level(Role::Member) {
|
|
perms.extend_from_slice(&[2, 3, 4, 14, 15, 16]);
|
|
}
|
|
if level >= role_level(Role::Moderator) {
|
|
perms.extend_from_slice(&[5, 6, 7, 12, 17, 18]);
|
|
}
|
|
if level >= role_level(Role::Admin) {
|
|
perms.extend_from_slice(&[8, 9, 10, 11, 13, 19]);
|
|
}
|
|
|
|
perms.sort();
|
|
perms.dedup();
|
|
perms
|
|
}
|
|
|
|
fn overwrite_to_proto(o: ChannelPermissionOverwrite) -> PermissionOverwrite {
|
|
PermissionOverwrite {
|
|
id: o.id.to_string(),
|
|
channel_id: o.channel_id.to_string(),
|
|
target_type: o.target_type.as_str().to_string(),
|
|
target_id: o.target_id.to_string(),
|
|
allow: o
|
|
.allow
|
|
.iter()
|
|
.map(|p| Self::str_to_im_permission(p))
|
|
.collect(),
|
|
deny: o
|
|
.deny
|
|
.iter()
|
|
.map(|p| Self::str_to_im_permission(p))
|
|
.collect(),
|
|
created_at: o.created_at.to_rfc3339(),
|
|
updated_at: o.updated_at.to_rfc3339(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[tonic::async_trait]
|
|
impl PermissionService for PermissionGrpcService {
|
|
async fn check_permission(
|
|
&self,
|
|
request: Request<CheckPermissionRequest>,
|
|
) -> Result<Response<CheckPermissionResponse>, Status> {
|
|
let req = request.into_inner();
|
|
let channel_id = Self::parse_uuid(&req.channel_id, "channel_id")?;
|
|
let user_uid = Self::parse_uuid(&req.user_id, "user_id")?;
|
|
|
|
let channel = self
|
|
.service
|
|
.im
|
|
.resolve_channel(channel_id)
|
|
.await
|
|
.map_err(|e| Status::not_found(e.to_string()))?;
|
|
|
|
let role = self
|
|
.service
|
|
.im
|
|
.channel_member_role(channel.id, user_uid)
|
|
.await
|
|
.map_err(|e| Status::internal(e.to_string()))?;
|
|
|
|
let required_role = Self::permission_requires_role(req.permission);
|
|
let allowed = role_level(role) >= role_level(required_role);
|
|
|
|
Ok(Response::new(CheckPermissionResponse {
|
|
allowed,
|
|
role: role.as_str().to_string(),
|
|
}))
|
|
}
|
|
|
|
async fn get_permissions(
|
|
&self,
|
|
request: Request<GetPermissionsRequest>,
|
|
) -> Result<Response<GetPermissionsResponse>, Status> {
|
|
let req = request.into_inner();
|
|
let channel_id = Self::parse_uuid(&req.channel_id, "channel_id")?;
|
|
let user_uid = Self::parse_uuid(&req.user_id, "user_id")?;
|
|
|
|
let channel = self
|
|
.service
|
|
.im
|
|
.resolve_channel(channel_id)
|
|
.await
|
|
.map_err(|e| Status::not_found(e.to_string()))?;
|
|
|
|
let role = self
|
|
.service
|
|
.im
|
|
.channel_member_role(channel.id, user_uid)
|
|
.await
|
|
.map_err(|e| Status::internal(e.to_string()))?;
|
|
|
|
let permissions = Self::role_to_permissions(role);
|
|
|
|
Ok(Response::new(GetPermissionsResponse {
|
|
permissions,
|
|
role: role.as_str().to_string(),
|
|
}))
|
|
}
|
|
|
|
async fn set_permission_overwrite(
|
|
&self,
|
|
request: Request<SetPermissionOverwriteRequest>,
|
|
) -> Result<Response<SetPermissionOverwriteResponse>, Status> {
|
|
let req = request.into_inner();
|
|
let channel_id = Self::parse_uuid(&req.channel_id, "channel_id")?;
|
|
let target_id = Self::parse_uuid(&req.target_id, "target_id")?;
|
|
let target_type: OverwriteTarget = req.target_type.parse().unwrap_or(OverwriteTarget::Unknown);
|
|
|
|
let allow: Vec<String> = req
|
|
.allow
|
|
.into_iter()
|
|
.map(|v| Self::im_permission_to_str(v).to_string())
|
|
.collect();
|
|
let deny: Vec<String> = req
|
|
.deny
|
|
.into_iter()
|
|
.map(|v| Self::im_permission_to_str(v).to_string())
|
|
.collect();
|
|
|
|
let now = chrono::Utc::now();
|
|
let id = Uuid::new_v4();
|
|
|
|
let overwrite = sqlx::query_as::<_, ChannelPermissionOverwrite>(
|
|
"INSERT INTO channel_permission_overwrite \
|
|
(id, channel_id, target_type, target_id, allow, deny, created_by, created_at, updated_at) \
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) \
|
|
ON CONFLICT (channel_id, target_type, target_id) \
|
|
DO UPDATE SET allow = $5, deny = $6, updated_at = $9 \
|
|
RETURNING id, channel_id, target_type, target_id, allow, deny, created_by, created_at, updated_at",
|
|
)
|
|
.bind(id)
|
|
.bind(channel_id)
|
|
.bind(target_type)
|
|
.bind(target_id)
|
|
.bind(&allow)
|
|
.bind(&deny)
|
|
.bind(Uuid::nil())
|
|
.bind(now)
|
|
.bind(now)
|
|
.fetch_one(self.service.ctx.db.writer())
|
|
.await
|
|
.map_err(|e| Status::internal(e.to_string()))?;
|
|
|
|
Ok(Response::new(SetPermissionOverwriteResponse {
|
|
overwrite: Some(Self::overwrite_to_proto(overwrite)),
|
|
}))
|
|
}
|
|
|
|
async fn get_permission_overwrites(
|
|
&self,
|
|
request: Request<GetPermissionOverwritesRequest>,
|
|
) -> Result<Response<GetPermissionOverwritesResponse>, Status> {
|
|
let req = request.into_inner();
|
|
let channel_id = Self::parse_uuid(&req.channel_id, "channel_id")?;
|
|
|
|
let overwrites = sqlx::query_as::<_, ChannelPermissionOverwrite>(
|
|
"SELECT id, channel_id, target_type, target_id, allow, deny, created_by, created_at, updated_at \
|
|
FROM channel_permission_overwrite WHERE channel_id = $1",
|
|
)
|
|
.bind(channel_id)
|
|
.fetch_all(self.service.ctx.db.reader())
|
|
.await
|
|
.map_err(|e| Status::internal(e.to_string()))?;
|
|
|
|
let proto_overwrites: Vec<_> = overwrites
|
|
.into_iter()
|
|
.map(Self::overwrite_to_proto)
|
|
.collect();
|
|
|
|
Ok(Response::new(GetPermissionOverwritesResponse {
|
|
overwrites: proto_overwrites,
|
|
}))
|
|
}
|
|
|
|
async fn delete_permission_overwrite(
|
|
&self,
|
|
request: Request<DeletePermissionOverwriteRequest>,
|
|
) -> Result<Response<DeletePermissionOverwriteResponse>, Status> {
|
|
let req = request.into_inner();
|
|
let channel_id = Self::parse_uuid(&req.channel_id, "channel_id")?;
|
|
let target_id = Self::parse_uuid(&req.target_id, "target_id")?;
|
|
let target_type: OverwriteTarget = req.target_type.parse().unwrap_or(OverwriteTarget::Unknown);
|
|
|
|
sqlx::query(
|
|
"DELETE FROM channel_permission_overwrite \
|
|
WHERE channel_id = $1 AND target_type = $2 AND target_id = $3",
|
|
)
|
|
.bind(channel_id)
|
|
.bind(target_type)
|
|
.bind(target_id)
|
|
.execute(self.service.ctx.db.writer())
|
|
.await
|
|
.map_err(|e| Status::internal(e.to_string()))?;
|
|
|
|
Ok(Response::new(DeletePermissionOverwriteResponse {}))
|
|
}
|
|
|
|
async fn resolve_channel(
|
|
&self,
|
|
request: Request<ResolveChannelRequest>,
|
|
) -> Result<Response<ResolveChannelResponse>, Status> {
|
|
let req = request.into_inner();
|
|
let channel_id = Self::parse_uuid(&req.channel_id, "channel_id")?;
|
|
|
|
let channel = self
|
|
.service
|
|
.im
|
|
.resolve_channel(channel_id)
|
|
.await
|
|
.map_err(|e| Status::not_found(e.to_string()))?;
|
|
|
|
Ok(Response::new(ResolveChannelResponse {
|
|
channel_id: channel.id.to_string(),
|
|
workspace_id: channel.workspace_id.to_string(),
|
|
name: channel.name,
|
|
visibility: channel.visibility.as_str().to_string(),
|
|
channel_type: channel.channel_type.as_str().to_string(),
|
|
read_only: channel.read_only,
|
|
archived: channel.archived,
|
|
created_by: Some(channel.created_by.to_string()),
|
|
}))
|
|
}
|
|
|
|
async fn ensure_readable(
|
|
&self,
|
|
request: Request<EnsureReadableRequest>,
|
|
) -> Result<Response<EnsureReadableResponse>, Status> {
|
|
let req = request.into_inner();
|
|
let channel_id = Self::parse_uuid(&req.channel_id, "channel_id")?;
|
|
let user_uid = Self::parse_uuid(&req.user_id, "user_id")?;
|
|
|
|
let channel = self
|
|
.service
|
|
.im
|
|
.resolve_channel(channel_id)
|
|
.await
|
|
.map_err(|e| Status::not_found(e.to_string()))?;
|
|
|
|
let allowed = self
|
|
.service
|
|
.im
|
|
.ensure_channel_readable(user_uid, &channel)
|
|
.await
|
|
.is_ok();
|
|
|
|
Ok(Response::new(EnsureReadableResponse { allowed }))
|
|
}
|
|
}
|