fix(auth): check handle
This commit is contained in:
34
Cargo.lock
generated
34
Cargo.lock
generated
@@ -90,6 +90,7 @@ name = "auth-kit"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
|
"axum-extra",
|
||||||
"base64",
|
"base64",
|
||||||
"common-telemetry",
|
"common-telemetry",
|
||||||
"dashmap",
|
"dashmap",
|
||||||
@@ -183,6 +184,28 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "axum-extra"
|
||||||
|
version = "0.12.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fef252edff26ddba56bbcdf2ee3307b8129acb86f5749b68990c168a6fcc9c76"
|
||||||
|
dependencies = [
|
||||||
|
"axum",
|
||||||
|
"axum-core",
|
||||||
|
"bytes",
|
||||||
|
"cookie",
|
||||||
|
"futures-core",
|
||||||
|
"futures-util",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"mime",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "backon"
|
name = "backon"
|
||||||
version = "1.6.0"
|
version = "1.6.0"
|
||||||
@@ -390,6 +413,17 @@ dependencies = [
|
|||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cookie"
|
||||||
|
version = "0.18.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
|
||||||
|
dependencies = [
|
||||||
|
"percent-encoding",
|
||||||
|
"time",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.9.4"
|
version = "0.9.4"
|
||||||
|
|||||||
@@ -37,8 +37,8 @@
|
|||||||
"code": 0,
|
"code": 0,
|
||||||
"message": "Success",
|
"message": "Success",
|
||||||
"data": {
|
"data": {
|
||||||
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImxvY2FsLWRldiJ9.eyJzdWIiOiI3YTgzYmJjMC0yZjA1LTQwMDItOGFmZC0yYWI1M2RkZDMxNWIiLCJ0ZW5hbnRfaWQiOiI0ZDc3OTQxNC1kYTA0LTQ5YzMtYjM0Mi1kYWJmOTNiNmExMTkiLCJleHAiOjE3NzAwOTc2NzYsImlhdCI6MTc3MDA5Njc3NiwiaXNzIjoiaWFtLXNlcnZpY2UiLCJyb2xlcyI6WyJBZG1pbiIsIm1hbmFnZXIiXSwicGVybWlzc2lvbnMiOlsiY21zOmFydGljbGU6Y3JlYXRlIiwiY21zOmFydGljbGU6ZWRpdCIsImNtczphcnRpY2xlOnB1Ymxpc2giLCJjbXM6Y2F0ZWdvcnk6bWFuYWdlIiwiY21zOm1lZGlhOm1hbmFnZSIsImNtczpzZXR0aW5nczptYW5hZ2UiLCJpYW06Y2xpZW50OnJlYWQiLCJpYW06Y2xpZW50OndyaXRlIiwicm9sZTpyZWFkIiwicm9sZTp3cml0ZSIsInRlbmFudDpyZWFkIiwidGVuYW50OndyaXRlIiwidXNlcjpwYXNzd29yZDpyZXNldDphbnkiLCJ1c2VyOnJlYWQiLCJ1c2VyOndyaXRlIl0sImFwcHMiOlsiY21zIl0sImFwcHNfdmVyc2lvbiI6MX0.Tt7fEQj8wtS5XzJ-GuwRF5yiOrtGaRr9P_V5RqNZagLXj0eRikf9U4oNkFS2uy8Pp75Ks4jL816DzeLXQsXZJfWEtvlTp0QmwyOhYIN1p-yyGS9Pitl0gb7wobjStGDyMSD-ctbHgEsU41qLrQd6ZQkpFl2IpaCSfCN0JZCpc7_3BeI6YLwAw_K4-TFF_1OTNRPm4sT3RnEZYOHXm6EcOUk-MsDBy1itADCdEEUdCSoslK6FGHIpbhkgA56Z7Qy5909BxXW34I21c2rZX-R_iB9q_eKzWd0GqBMIZi33ITbRy4F7_CtCQSwFNUt6-lvVvzXYLHsVQchcpOdYtj3h6w",
|
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImRlZmF1bHQifQ.eyJzdWIiOiI3YTgzYmJjMC0yZjA1LTQwMDItOGFmZC0yYWI1M2RkZDMxNWIiLCJ0ZW5hbnRfaWQiOiI0ZDc3OTQxNC1kYTA0LTQ5YzMtYjM0Mi1kYWJmOTNiNmExMTkiLCJleHAiOjE3NzA3NzU0NDYsImlhdCI6MTc3MDc3NDU0NiwiaXNzIjoiaWFtLXNlcnZpY2UiLCJyb2xlcyI6WyJBZG1pbiIsIm1hbmFnZXIiXSwicGVybWlzc2lvbnMiOlsiY21zOmFydGljbGU6Y3JlYXRlIiwiY21zOmFydGljbGU6ZWRpdCIsImNtczphcnRpY2xlOnB1Ymxpc2giLCJjbXM6Y2F0ZWdvcnk6bWFuYWdlIiwiY21zOm1lZGlhOm1hbmFnZSIsImNtczpzZXR0aW5nczptYW5hZ2UiLCJpYW06Y2xpZW50OnJlYWQiLCJpYW06Y2xpZW50OndyaXRlIiwicm9sZTpyZWFkIiwicm9sZTp3cml0ZSIsInRlbmFudDpyZWFkIiwidGVuYW50OndyaXRlIiwidXNlcjpwYXNzd29yZDpyZXNldDphbnkiLCJ1c2VyOnJlYWQiLCJ1c2VyOndyaXRlIl0sImFwcHMiOlsiY21zIl0sImFwcHNfdmVyc2lvbiI6MX0.yPCz0CgX4qc1AqFM_yi-IIthVRBzYykFAIdJQmB0O6EEWSMutG3WtW2L9lvvBEajmJuje-PIxj-UwpS6tInzUR0H7BKdqOwmao9CKJkaOjp6aSfOZUxQ90kaR1PnJtjmkI8r6rorvWtTyoLIMzwbHisXjMoYxZ5-JTrOmXdYs_MT50cn-tzb7eV_eM7g77J82-vxTVQq0olKA7rLpOFw2B4hHOoNjniL8Tas8zupHakVZzW7G1Q25tveqJtck9rDfEm2KpswKC_Kh4-kk1E5M1rESbpzM0h9VENTd2EPgxvOcIjPtczkMp89qlrovQqZteCCyDDSWnfGPWGsVrm7tQ",
|
||||||
"refresh_token": "7483624e8942aee112e62b2b58ec902fb01731dd7b098b4af6001e350b2303f0",
|
"refresh_token": "56923a401cf9bbf08f988dfff05b28aabbff0814a02f7760b5fbf34f56df8ed8",
|
||||||
"token_type": "Bearer",
|
"token_type": "Bearer",
|
||||||
"expires_in": 900
|
"expires_in": 900
|
||||||
}
|
}
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
```
|
```
|
||||||
|
|
||||||
`user1@example.com`
|
`user1@example.com`
|
||||||
`user1secret`
|
`user5secret`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ use sqlx::PgPool;
|
|||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::models::{PermissionExpr, PermissionExprItem};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct AuthorizationService {
|
pub struct AuthorizationService {
|
||||||
pool: PgPool,
|
pool: PgPool,
|
||||||
@@ -88,6 +90,17 @@ impl AuthorizationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn check_permission_expr(
|
||||||
|
&self,
|
||||||
|
tenant_id: Uuid,
|
||||||
|
user_id: Uuid,
|
||||||
|
expr: &PermissionExpr,
|
||||||
|
) -> Result<bool, AppError> {
|
||||||
|
validate_expr_depth(expr, 32)?;
|
||||||
|
let permissions = self.list_permissions_for_user(tenant_id, user_id).await?;
|
||||||
|
Ok(evaluate_expr(&permissions, expr))
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip(self))]
|
||||||
pub async fn list_platform_permissions_for_user(
|
pub async fn list_platform_permissions_for_user(
|
||||||
&self,
|
&self,
|
||||||
@@ -122,3 +135,45 @@ impl AuthorizationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn validate_expr_depth(expr: &PermissionExpr, max_depth: usize) -> Result<(), AppError> {
|
||||||
|
fn walk(expr: &PermissionExpr, depth: usize, max_depth: usize) -> Result<(), AppError> {
|
||||||
|
if depth > max_depth {
|
||||||
|
return Err(AppError::BadRequest("permission expr too deep".into()));
|
||||||
|
}
|
||||||
|
let items: &[PermissionExprItem] = match expr {
|
||||||
|
PermissionExpr::Any(x) => &x.any,
|
||||||
|
PermissionExpr::All(x) => &x.all,
|
||||||
|
};
|
||||||
|
for item in items {
|
||||||
|
if let PermissionExprItem::Expr(e) = item {
|
||||||
|
walk(e, depth + 1, max_depth)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
walk(expr, 1, max_depth)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn evaluate_expr(user_permissions: &[String], expr: &PermissionExpr) -> bool {
|
||||||
|
let items: &[PermissionExprItem] = match expr {
|
||||||
|
PermissionExpr::Any(x) => &x.any,
|
||||||
|
PermissionExpr::All(x) => &x.all,
|
||||||
|
};
|
||||||
|
|
||||||
|
match expr {
|
||||||
|
PermissionExpr::Any(_) => items.iter().any(|item| match item {
|
||||||
|
PermissionExprItem::Permission(required) => user_permissions
|
||||||
|
.iter()
|
||||||
|
.any(|granted| matches_permission(granted.as_str(), required.as_str())),
|
||||||
|
PermissionExprItem::Expr(e) => evaluate_expr(user_permissions, e),
|
||||||
|
}),
|
||||||
|
PermissionExpr::All(_) => items.iter().all(|item| match item {
|
||||||
|
PermissionExprItem::Permission(required) => user_permissions
|
||||||
|
.iter()
|
||||||
|
.any(|granted| matches_permission(granted.as_str(), required.as_str())),
|
||||||
|
PermissionExprItem::Expr(e) => evaluate_expr(user_permissions, e),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -252,6 +252,13 @@ impl ClientService {
|
|||||||
let requested_path = requested.path().to_string();
|
let requested_path = requested.path().to_string();
|
||||||
let requested_scheme = requested.scheme().to_string();
|
let requested_scheme = requested.scheme().to_string();
|
||||||
|
|
||||||
|
if arr.is_empty()
|
||||||
|
&& cfg!(debug_assertions)
|
||||||
|
&& (requested_host == "localhost" || requested_host == "127.0.0.1")
|
||||||
|
{
|
||||||
|
return Ok(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
let allowed = arr.iter().filter_map(|x| x.as_str()).any(|raw_allowed| {
|
let allowed = arr.iter().filter_map(|x| x.as_str()).any(|raw_allowed| {
|
||||||
let allowed_norm = match self.normalize_redirect_uri(raw_allowed) {
|
let allowed_norm = match self.normalize_redirect_uri(raw_allowed) {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
@@ -265,7 +272,14 @@ impl ClientService {
|
|||||||
let host = allowed_url.host_str().unwrap_or_default();
|
let host = allowed_url.host_str().unwrap_or_default();
|
||||||
let port = allowed_url.port_or_known_default().unwrap_or(0);
|
let port = allowed_url.port_or_known_default().unwrap_or(0);
|
||||||
|
|
||||||
if allowed_url.scheme() != requested_scheme
|
let scheme_ok = allowed_url.scheme() == requested_scheme
|
||||||
|
|| (cfg!(debug_assertions)
|
||||||
|
&& (requested_host == "localhost" || requested_host == "127.0.0.1")
|
||||||
|
&& host == requested_host
|
||||||
|
&& ((allowed_url.scheme() == "https" && requested_scheme == "http")
|
||||||
|
|| (allowed_url.scheme() == "http" && requested_scheme == "https")));
|
||||||
|
|
||||||
|
if !scheme_ok
|
||||||
|| host != requested_host
|
|| host != requested_host
|
||||||
|| port != requested_port
|
|| port != requested_port
|
||||||
|| allowed_url.path() != requested_path
|
|| allowed_url.path() != requested_path
|
||||||
@@ -376,4 +390,3 @@ impl ClientService {
|
|||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -361,7 +361,6 @@ pub struct RefreshTokenRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, ToSchema, IntoParams)]
|
#[derive(Debug, Deserialize, ToSchema, IntoParams)]
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct LoginCodeRequest {
|
pub struct LoginCodeRequest {
|
||||||
#[schema(default = "", example = "user@example.com")]
|
#[schema(default = "", example = "user@example.com")]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -371,17 +370,18 @@ pub struct LoginCodeRequest {
|
|||||||
pub password: String,
|
pub password: String,
|
||||||
#[schema(default = "", example = "cms")]
|
#[schema(default = "", example = "cms")]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
#[serde(alias = "clientId")]
|
||||||
pub client_id: String,
|
pub client_id: String,
|
||||||
#[schema(
|
#[schema(
|
||||||
default = "",
|
default = "",
|
||||||
example = "https://cms-api.example.com/auth/callback?next=https%3A%2F%2Fcms.example.com%2F"
|
example = "https://cms-api.example.com/auth/callback?next=https%3A%2F%2Fcms.example.com%2F"
|
||||||
)]
|
)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
#[serde(alias = "redirectUri")]
|
||||||
pub redirect_uri: String,
|
pub redirect_uri: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, ToSchema, IntoParams)]
|
#[derive(Debug, Serialize, ToSchema, IntoParams)]
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct LoginCodeResponse {
|
pub struct LoginCodeResponse {
|
||||||
#[schema(
|
#[schema(
|
||||||
default = "",
|
default = "",
|
||||||
@@ -395,21 +395,21 @@ pub struct LoginCodeResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, ToSchema, IntoParams)]
|
#[derive(Debug, Deserialize, ToSchema, IntoParams)]
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct Code2TokenRequest {
|
pub struct Code2TokenRequest {
|
||||||
#[schema(default = "", example = "one_time_code_jwt")]
|
#[schema(default = "", example = "one_time_code_jwt")]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub code: String,
|
pub code: String,
|
||||||
#[schema(default = "", example = "cms")]
|
#[schema(default = "", example = "cms")]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
#[serde(alias = "clientId")]
|
||||||
pub client_id: String,
|
pub client_id: String,
|
||||||
#[schema(default = "", example = "client_secret")]
|
#[schema(default = "", example = "client_secret")]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
#[serde(alias = "clientSecret")]
|
||||||
pub client_secret: String,
|
pub client_secret: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, ToSchema, IntoParams)]
|
#[derive(Debug, Serialize, ToSchema, IntoParams)]
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct Code2TokenResponse {
|
pub struct Code2TokenResponse {
|
||||||
#[schema(default = "", example = "access_token")]
|
#[schema(default = "", example = "access_token")]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -432,20 +432,20 @@ pub struct Code2TokenResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, ToSchema, IntoParams)]
|
#[derive(Debug, Deserialize, ToSchema, IntoParams)]
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct CreateClientRequest {
|
pub struct CreateClientRequest {
|
||||||
#[schema(default = "", example = "cms")]
|
#[schema(default = "", example = "cms")]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
#[serde(alias = "clientId")]
|
||||||
pub client_id: String,
|
pub client_id: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
#[schema(example = "[\"https://cms.example.com/auth/callback\"]")]
|
#[schema(example = "[\"https://cms.example.com/auth/callback\"]")]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
#[serde(alias = "redirectUris")]
|
||||||
pub redirect_uris: Option<Vec<String>>,
|
pub redirect_uris: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, ToSchema, IntoParams)]
|
#[derive(Debug, Serialize, ToSchema, IntoParams)]
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct CreateClientResponse {
|
pub struct CreateClientResponse {
|
||||||
#[schema(default = "", example = "cms")]
|
#[schema(default = "", example = "cms")]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -456,7 +456,6 @@ pub struct CreateClientResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, ToSchema, IntoParams)]
|
#[derive(Debug, Serialize, ToSchema, IntoParams)]
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct RotateClientSecretResponse {
|
pub struct RotateClientSecretResponse {
|
||||||
#[schema(default = "", example = "cms")]
|
#[schema(default = "", example = "cms")]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -467,7 +466,6 @@ pub struct RotateClientSecretResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, ToSchema, IntoParams)]
|
#[derive(Debug, Serialize, ToSchema, IntoParams)]
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct ClientSummary {
|
pub struct ClientSummary {
|
||||||
#[schema(default = "", example = "cms")]
|
#[schema(default = "", example = "cms")]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -476,6 +474,7 @@ pub struct ClientSummary {
|
|||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
#[schema(example = "[\"https://cms.example.com/auth/callback\"]")]
|
#[schema(example = "[\"https://cms.example.com/auth/callback\"]")]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
#[serde(alias = "redirectUris")]
|
||||||
pub redirect_uris: Vec<String>,
|
pub redirect_uris: Vec<String>,
|
||||||
#[schema(default = "", example = "2026-02-02T12:00:00Z")]
|
#[schema(default = "", example = "2026-02-02T12:00:00Z")]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -486,10 +485,10 @@ pub struct ClientSummary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, ToSchema, IntoParams)]
|
#[derive(Debug, Deserialize, ToSchema, IntoParams)]
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct UpdateClientRedirectUrisRequest {
|
pub struct UpdateClientRedirectUrisRequest {
|
||||||
#[schema(example = "[\"https://cms.example.com/auth/callback\"]")]
|
#[schema(example = "[\"https://cms.example.com/auth/callback\"]")]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
#[serde(alias = "redirectUris")]
|
||||||
pub redirect_uris: Vec<String>,
|
pub redirect_uris: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -552,3 +551,39 @@ pub struct AuthorizationCheckResponse {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub allowed: bool,
|
pub allowed: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, ToSchema, IntoParams)]
|
||||||
|
pub struct AuthorizationExprCheckRequest {
|
||||||
|
pub expr: PermissionExpr,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, ToSchema, IntoParams)]
|
||||||
|
pub struct AuthorizationExprCheckResponse {
|
||||||
|
pub allowed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, ToSchema)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct AnyExpr {
|
||||||
|
pub any: Vec<PermissionExprItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, ToSchema)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct AllExpr {
|
||||||
|
pub all: Vec<PermissionExprItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, ToSchema)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum PermissionExpr {
|
||||||
|
Any(AnyExpr),
|
||||||
|
All(AllExpr),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, ToSchema)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum PermissionExprItem {
|
||||||
|
Permission(String),
|
||||||
|
Expr(Box<PermissionExpr>),
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ pub fn routes() -> Vec<(&'static str, &'static str)> {
|
|||||||
("POST", "/internal/auth/code2token"),
|
("POST", "/internal/auth/code2token"),
|
||||||
("GET", "/me/permissions"),
|
("GET", "/me/permissions"),
|
||||||
("POST", "/authorize/check"),
|
("POST", "/authorize/check"),
|
||||||
|
("POST", "/authorize/check-expr"),
|
||||||
("GET", "/users"),
|
("GET", "/users"),
|
||||||
("GET", "/users/{id}"),
|
("GET", "/users/{id}"),
|
||||||
("PATCH", "/users/{id}"),
|
("PATCH", "/users/{id}"),
|
||||||
@@ -144,6 +145,10 @@ pub fn build_app(state: AppState) -> Router {
|
|||||||
"/authorize/check",
|
"/authorize/check",
|
||||||
post(authorization::authorization_check_handler),
|
post(authorization::authorization_check_handler),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/authorize/check-expr",
|
||||||
|
post(authorization::authorization_check_expr_handler),
|
||||||
|
)
|
||||||
.route("/users", get(user::list_users_handler))
|
.route("/users", get(user::list_users_handler))
|
||||||
.route(
|
.route(
|
||||||
"/users/me/password/reset",
|
"/users/me/password/reset",
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
use crate::middleware::TenantId;
|
use crate::middleware::TenantId;
|
||||||
use crate::middleware::auth::AuthContext;
|
use crate::middleware::auth::AuthContext;
|
||||||
use crate::models::{AuthorizationCheckRequest, AuthorizationCheckResponse};
|
use crate::models::{
|
||||||
|
AuthorizationCheckRequest, AuthorizationCheckResponse, AuthorizationExprCheckRequest,
|
||||||
|
AuthorizationExprCheckResponse,
|
||||||
|
};
|
||||||
use crate::presentation::http::state::AppState;
|
use crate::presentation::http::state::AppState;
|
||||||
use axum::{Json, extract::State};
|
use axum::{Json, extract::State};
|
||||||
use common_telemetry::{AppError, AppResponse};
|
use common_telemetry::{AppError, AppResponse};
|
||||||
@@ -89,3 +92,45 @@ pub async fn authorization_check_handler(
|
|||||||
|
|
||||||
Ok(AppResponse::ok(AuthorizationCheckResponse { allowed }))
|
Ok(AppResponse::ok(AuthorizationCheckResponse { allowed }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/authorize/check-expr",
|
||||||
|
tag = "Policy",
|
||||||
|
security(
|
||||||
|
("bearer_auth" = [])
|
||||||
|
),
|
||||||
|
request_body = AuthorizationExprCheckRequest,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "鉴权校验结果", body = AuthorizationExprCheckResponse),
|
||||||
|
(status = 400, description = "表达式不合法"),
|
||||||
|
(status = 401, description = "未认证"),
|
||||||
|
(status = 403, description = "租户不匹配")
|
||||||
|
),
|
||||||
|
params(
|
||||||
|
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)"),
|
||||||
|
("X-Tenant-ID" = String, Header, description = "租户 UUID(可选,若提供需与 Token 中 tenant_id 一致)")
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
#[instrument(skip(state, body))]
|
||||||
|
pub async fn authorization_check_expr_handler(
|
||||||
|
TenantId(tenant_id): TenantId,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
AuthContext {
|
||||||
|
tenant_id: auth_tenant_id,
|
||||||
|
user_id,
|
||||||
|
..
|
||||||
|
}: AuthContext,
|
||||||
|
Json(body): Json<AuthorizationExprCheckRequest>,
|
||||||
|
) -> Result<AppResponse<AuthorizationExprCheckResponse>, AppError> {
|
||||||
|
if auth_tenant_id != tenant_id {
|
||||||
|
return Err(AppError::PermissionDenied("tenant:mismatch".into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let allowed = state
|
||||||
|
.authorization_service
|
||||||
|
.check_permission_expr(tenant_id, user_id, &body.expr)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(AppResponse::ok(AuthorizationExprCheckResponse { allowed }))
|
||||||
|
}
|
||||||
|
|||||||
27
tests/authorization_expr.rs
Normal file
27
tests/authorization_expr.rs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
use iam_service::models::{AuthorizationExprCheckRequest, PermissionExpr};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deserialize_any_expr() {
|
||||||
|
let json = r#"{ "expr": { "any": ["cms:article:edit", "cms:article:create"] } }"#;
|
||||||
|
let parsed: AuthorizationExprCheckRequest = serde_json::from_str(json).unwrap();
|
||||||
|
match parsed.expr {
|
||||||
|
PermissionExpr::Any(x) => {
|
||||||
|
assert_eq!(x.any.len(), 2);
|
||||||
|
}
|
||||||
|
_ => panic!("expected any"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deserialize_nested_expr() {
|
||||||
|
let json =
|
||||||
|
r#"{ "expr": { "all": ["cms:article:edit", { "any": ["cms:article:create", "cms:article:publish"] }] } }"#;
|
||||||
|
let parsed: AuthorizationExprCheckRequest = serde_json::from_str(json).unwrap();
|
||||||
|
match parsed.expr {
|
||||||
|
PermissionExpr::All(x) => {
|
||||||
|
assert_eq!(x.all.len(), 2);
|
||||||
|
}
|
||||||
|
_ => panic!("expected all"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
use axum::body::Body;
|
use axum::body::Body;
|
||||||
use axum::http::{Request, StatusCode};
|
use axum::http::{Request, StatusCode};
|
||||||
use iam_service::presentation::http::api;
|
|
||||||
use iam_service::presentation::http::state::AppState;
|
|
||||||
use iam_service::infrastructure::repositories::tenant_config_repo::TenantConfigRepoPg;
|
|
||||||
use iam_service::models::CreateTenantRequest;
|
|
||||||
use iam_service::models::CreateUserRequest;
|
|
||||||
use iam_service::application::services::{
|
use iam_service::application::services::{
|
||||||
AppService, AuthService, AuthorizationService, ClientService, PermissionService, RoleService,
|
AppService, AuthService, AuthorizationService, ClientService, PermissionService, RoleService,
|
||||||
TenantService, UserService,
|
TenantService, UserService,
|
||||||
};
|
};
|
||||||
|
use iam_service::constants::CANONICAL_BASE;
|
||||||
|
use iam_service::infrastructure::repositories::tenant_config_repo::TenantConfigRepoPg;
|
||||||
|
use iam_service::models::CreateTenantRequest;
|
||||||
|
use iam_service::models::CreateUserRequest;
|
||||||
|
use iam_service::presentation::http::api;
|
||||||
|
use iam_service::presentation::http::state::AppState;
|
||||||
use redis::aio::ConnectionManager;
|
use redis::aio::ConnectionManager;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use tower::ServiceExt;
|
use tower::ServiceExt;
|
||||||
@@ -106,7 +107,7 @@ async fn code2token_modes_requirements() -> Result<(), Box<dyn std::error::Error
|
|||||||
.oneshot(
|
.oneshot(
|
||||||
Request::builder()
|
Request::builder()
|
||||||
.method("POST")
|
.method("POST")
|
||||||
.uri("/api/v1/auth/login-code")
|
.uri(format!("{}/auth/login-code", CANONICAL_BASE))
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.header("X-Tenant-ID", tenant.id.to_string())
|
.header("X-Tenant-ID", tenant.id.to_string())
|
||||||
.body(Body::from(login_code_req.to_string()))?,
|
.body(Body::from(login_code_req.to_string()))?,
|
||||||
@@ -134,7 +135,7 @@ async fn code2token_modes_requirements() -> Result<(), Box<dyn std::error::Error
|
|||||||
.oneshot(
|
.oneshot(
|
||||||
Request::builder()
|
Request::builder()
|
||||||
.method("POST")
|
.method("POST")
|
||||||
.uri("/api/v1/auth/code2token")
|
.uri(format!("{}/auth/code2token", CANONICAL_BASE))
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.header("X-Tenant-ID", tenant.id.to_string())
|
.header("X-Tenant-ID", tenant.id.to_string())
|
||||||
.body(Body::from(code2token_req.to_string()))?,
|
.body(Body::from(code2token_req.to_string()))?,
|
||||||
@@ -152,7 +153,7 @@ async fn code2token_modes_requirements() -> Result<(), Box<dyn std::error::Error
|
|||||||
.oneshot(
|
.oneshot(
|
||||||
Request::builder()
|
Request::builder()
|
||||||
.method("POST")
|
.method("POST")
|
||||||
.uri("/api/v1/auth/code2token")
|
.uri(format!("{}/auth/code2token", CANONICAL_BASE))
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.body(Body::from(code2token_req_missing_tenant.to_string()))?,
|
.body(Body::from(code2token_req_missing_tenant.to_string()))?,
|
||||||
)
|
)
|
||||||
@@ -168,7 +169,7 @@ async fn code2token_modes_requirements() -> Result<(), Box<dyn std::error::Error
|
|||||||
.oneshot(
|
.oneshot(
|
||||||
Request::builder()
|
Request::builder()
|
||||||
.method("POST")
|
.method("POST")
|
||||||
.uri("/api/v1/internal/auth/code2token")
|
.uri(format!("{}/internal/auth/code2token", CANONICAL_BASE))
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.header("X-Internal-Token", internal_psk)
|
.header("X-Internal-Token", internal_psk)
|
||||||
.body(Body::from(code2token_req_internal.to_string()))?,
|
.body(Body::from(code2token_req_internal.to_string()))?,
|
||||||
|
|||||||
Reference in New Issue
Block a user