feat(space): expose internal authorization and organization members
- add `SpaceService::authorize_internal_repo/issue/pull/space` which resolve a resource to its dir and reuse the existing `Access`/`Capability` kernel instead of duplicating permission rules in the event gateway - add `internal_space_member_ids` plus the `ListMembers` rpc so event fan-out can address a space (organization) without reading business tables - register the chat/issue/pull/space authorization services on the generated tonic traits, keeping protocol conversion in `rpc/` only
ZhenYi committed
Added lib/space/src/rpc/mod.rs lib/space/src/rpc/mod.rs +165 −0 View lib/space/src/rpc/mod.rs
@@ -0,0 +1,165 @@
1//! 内部 RPC 的 space 域:把资源 id 解析到目录后复用权限内核(Access/Capability)。
2
3use internal::Gate;
4use internal::server::InternalRouter;
5use proto::internal::v1::issue_authorization_service_server::{
6 IssueAuthorizationService as IssueAuthorizationTrait, IssueAuthorizationServiceServer,
7};
8use proto::internal::v1::pull_authorization_service_server::{
9 PullAuthorizationService as PullAuthorizationTrait, PullAuthorizationServiceServer,
10};
11use proto::internal::v1::space_authorization_service_server::{
12 SpaceAuthorizationService as SpaceAuthorizationTrait, SpaceAuthorizationServiceServer,
13};
14use proto::internal::v1::{
15 AuthorizationDecision, AuthorizeIssueRequest, AuthorizePullRequest, AuthorizeResourceRequest,
16 SpaceMembersRequest, SpaceMembersResponse,
17};
18use tonic::{Request, Response, Status};
19
20use crate::errors::SpaceError;
21use crate::services::service::SpaceService;
22
23pub fn register(router: InternalRouter, service: &SpaceService, gate: Gate) -> InternalRouter {
24 let rpc = SpaceRpc {
25 service: service.clone(),
26 };
27 let router = router.add_service(IssueAuthorizationServiceServer::with_interceptor(
28 rpc.clone(),
29 gate.clone(),
30 ));
31 let router = router.add_service(PullAuthorizationServiceServer::with_interceptor(
32 rpc.clone(),
33 gate.clone(),
34 ));
35 let router = router.add_service(SpaceAuthorizationServiceServer::with_interceptor(
36 rpc.clone(),
37 gate.clone(),
38 ));
39 router
40}
41
42#[derive(Clone)]
43struct SpaceRpc {
44 service: SpaceService,
45}
46
47fn resource_id(value: &str) -> Result<uuid::Uuid, Status> {
48 value
49 .parse()
50 .map_err(|_| Status::invalid_argument("resource id must be a UUID"))
51}
52
53#[tonic::async_trait]
54impl IssueAuthorizationTrait for SpaceRpc {
55 async fn authorize_issue(
56 &self,
57 request: Request<AuthorizeIssueRequest>,
58 ) -> Result<Response<AuthorizationDecision>, Status> {
59 let _meta = internal::interceptor::meta(&request)?;
60 let request = request.into_inner();
61 let issue_id = resource_id(&request.issue_id)?;
62 let allowed = self
63 .service
64 .authorize_internal_issue(request.user_id, issue_id)
65 .await
66 .map_err(status_of)?;
67 Ok(Response::new(AuthorizationDecision {
68 allowed,
69 reason: String::new(),
70 resource_id: request.issue_id,
71 }))
72 }
73}
74
75#[tonic::async_trait]
76impl PullAuthorizationTrait for SpaceRpc {
77 async fn authorize_pull(
78 &self,
79 request: Request<AuthorizePullRequest>,
80 ) -> Result<Response<AuthorizationDecision>, Status> {
81 let _meta = internal::interceptor::meta(&request)?;
82 let request = request.into_inner();
83 let pull_id = resource_id(&request.pull_id)?;
84 let allowed = self
85 .service
86 .authorize_internal_pull(request.user_id, pull_id)
87 .await
88 .map_err(status_of)?;
89 Ok(Response::new(AuthorizationDecision {
90 allowed,
91 reason: String::new(),
92 resource_id: request.pull_id,
93 }))
94 }
95}
96
97#[tonic::async_trait]
98impl SpaceAuthorizationTrait for SpaceRpc {
99 async fn authorize_repo(
100 &self,
101 request: Request<AuthorizeResourceRequest>,
102 ) -> Result<Response<AuthorizationDecision>, Status> {
103 let _meta = internal::interceptor::meta(&request)?;
104 let request = request.into_inner();
105 let repo_id = resource_id(&request.repo_id)?;
106 let allowed = self
107 .service
108 .authorize_internal_repo(request.user_id, repo_id)
109 .await
110 .map_err(status_of)?;
111 Ok(Response::new(AuthorizationDecision {
112 allowed,
113 reason: String::new(),
114 resource_id: request.repo_id,
115 }))
116 }
117
118 async fn authorize_space(
119 &self,
120 request: Request<AuthorizeResourceRequest>,
121 ) -> Result<Response<AuthorizationDecision>, Status> {
122 let _meta = internal::interceptor::meta(&request)?;
123 let request = request.into_inner();
124 let space_id = resource_id(&request.space_id)?;
125 let allowed = self
126 .service
127 .authorize_internal_space(request.user_id, space_id)
128 .await
129 .map_err(status_of)?;
130 Ok(Response::new(AuthorizationDecision {
131 allowed,
132 reason: String::new(),
133 resource_id: request.space_id,
134 }))
135 }
136
137 async fn list_members(
138 &self,
139 request: Request<SpaceMembersRequest>,
140 ) -> Result<Response<SpaceMembersResponse>, Status> {
141 let _meta = internal::interceptor::meta(&request)?;
142 let request = request.into_inner();
143 let space_id = resource_id(&request.space_id)?;
144 let user_ids = self
145 .service
146 .internal_space_member_ids(space_id)
147 .await
148 .map_err(status_of)?;
149 Ok(Response::new(SpaceMembersResponse { user_ids }))
150 }
151}
152
153fn status_of(error: SpaceError) -> Status {
154 match error {
155 SpaceError::Unauthorized => Status::unauthenticated("space authorization rejected"),
156 SpaceError::Forbidden | SpaceError::NotFound | SpaceError::SpaceNotFound => {
157 Status::permission_denied("not allowed")
158 }
159 SpaceError::BadRequest(message) => Status::invalid_argument(message),
160 other => {
161 tracing::warn!(%other, "internal space authorization failed");
162 Status::internal("space authorization failed")
163 }
164 }
165}
Added lib/space/src/services/internal_authz.rs lib/space/src/services/internal_authz.rs +122 −0 View lib/space/src/services/internal_authz.rs
@@ -0,0 +1,122 @@
1use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
2use uuid::Uuid;
3
4use crate::access::{Capability, require};
5use crate::errors::{SpaceError, SpaceResult};
6use crate::services::service::SpaceService;
7
8fn access_error(error: export::AccessError) -> SpaceError {
9 SpaceError::Internal(error.to_string())
10}
11
12impl SpaceService {
13 pub async fn authorize_internal_repo(&self, user_id: i64, repo_id: Uuid) -> SpaceResult<bool> {
14 let repo = models::repo::repos::Entity::find_by_id(repo_id)
15 .one(self.access.database.read())
16 .await
17 .map_err(|error| SpaceError::Internal(error.to_string()))?
18 .ok_or(SpaceError::NotFound)?;
19 let Some(space) = self
20 .access
21 .for_dir(repo.dir_id)
22 .await
23 .map_err(access_error)?
24 else {
25 return Ok(false);
26 };
27 space
28 .can(
29 user_id,
30 Capability::RepositoryRead {
31 repository: repo.id,
32 private: repo.is_private,
33 },
34 )
35 .await
36 .map_err(access_error)
37 }
38
39 pub async fn authorize_internal_issue(
40 &self,
41 user_id: i64,
42 issue_id: Uuid,
43 ) -> SpaceResult<bool> {
44 let issue = models::issue::issues::Entity::find_by_id(issue_id)
45 .one(self.access.database.read())
46 .await
47 .map_err(|error| SpaceError::Internal(error.to_string()))?
48 .ok_or(SpaceError::NotFound)?;
49 let Some(space) = self
50 .access
51 .for_dir(issue.dir_id)
52 .await
53 .map_err(access_error)?
54 else {
55 return Ok(false);
56 };
57 space
58 .object_visible(user_id, issue.dir_id)
59 .await
60 .map_err(access_error)
61 }
62
63 pub async fn authorize_internal_pull(&self, user_id: i64, pull_id: Uuid) -> SpaceResult<bool> {
64 let pull = models::pull_request::pull_requests::Entity::find_by_id(pull_id)
65 .one(self.access.database.read())
66 .await
67 .map_err(|error| SpaceError::Internal(error.to_string()))?
68 .ok_or(SpaceError::NotFound)?;
69 let repo = models::repo::repos::Entity::find_by_id(pull.repo_id)
70 .one(self.access.database.read())
71 .await
72 .map_err(|error| SpaceError::Internal(error.to_string()))?
73 .ok_or(SpaceError::NotFound)?;
74 let Some(space) = self
75 .access
76 .for_dir(repo.dir_id)
77 .await
78 .map_err(access_error)?
79 else {
80 return Ok(false);
81 };
82 space
83 .can(
84 user_id,
85 Capability::RepositoryRead {
86 repository: repo.id,
87 private: repo.is_private,
88 },
89 )
90 .await
91 .map_err(access_error)
92 }
93
94 pub async fn authorize_internal_space(
95 &self,
96 user_id: i64,
97 space_id: Uuid,
98 ) -> SpaceResult<bool> {
99 let space = self.access.by_id(space_id).await.map_err(access_error)?;
100 require(&space, user_id, Capability::Read)
101 .await
102 .map(|()| true)
103 .or_else(|error| match error {
104 export::AccessError::Forbidden => Ok(false),
105 other => Err(access_error(other)),
106 })
107 }
108}
109
110impl SpaceService {
111 /// 内部事件扇出:列出空间(组织)的有效成员 id(未退出、未封禁)。
112 pub async fn internal_space_member_ids(&self, space_id: Uuid) -> SpaceResult<Vec<i64>> {
113 let rows = models::space::space_members::Entity::find()
114 .filter(models::space::space_members::Column::OrganizationId.eq(space_id))
115 .filter(models::space::space_members::Column::LeftAt.is_null())
116 .filter(models::space::space_members::Column::BannedAt.is_null())
117 .all(self.access.database.read())
118 .await
119 .map_err(|error| SpaceError::Internal(error.to_string()))?;
120 Ok(rows.into_iter().map(|row| row.user_id).collect())
121 }
122}
Modified lib/space/src/services/mod.rs lib/space/src/services/mod.rs +1 −0 View lib/space/src/services/mod.rs
@@ -3,6 +3,7 @@
33pub mod error;
44pub mod groups;
55pub mod images;
6pub mod internal_authz;
67pub mod invite;
78pub mod join;
89pub mod join_requests;
Modified lib/space/src/lib.rs lib/space/src/lib.rs +1 −0 View lib/space/src/lib.rs
@@ -5,6 +5,7 @@
55pub mod handlers;
66pub mod openapi;
77pub mod routes;
8pub mod rpc;
89pub mod services;
910pub mod share;
1011
Modified lib/space/Cargo.toml lib/space/Cargo.toml +2 −0 View lib/space/Cargo.toml
@@ -16,7 +16,9 @@
1616envelope = { workspace = true }
1717export = { workspace = true }
1818events = { workspace = true }
19internal = { workspace = true }
1920proto = { workspace = true }
21tonic = { workspace = true }
2022i18n = { workspace = true }
2123imaging = { workspace = true }
2224models = { workspace = true }