feat(role): role bind
This commit is contained in:
107
tests/refresh_token_smoke.rs
Normal file
107
tests/refresh_token_smoke.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
use hmac::{Hmac, Mac};
|
||||
use iam_service::models::{CreateTenantRequest, CreateUserRequest, LoginRequest};
|
||||
use iam_service::services::{AuthService, TenantService};
|
||||
use sha2::Sha256;
|
||||
use sqlx::PgPool;
|
||||
use uuid::Uuid;
|
||||
|
||||
fn fingerprint(pepper: &str, token: &str) -> String {
|
||||
let mut mac = Hmac::<Sha256>::new_from_slice(pepper.as_bytes()).unwrap();
|
||||
mac.update(token.as_bytes());
|
||||
hex::encode(mac.finalize().into_bytes())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn refresh_token_rotate_and_expire_cases()
|
||||
-> Result<(), Box<dyn std::error::Error>> {
|
||||
let database_url = match std::env::var("DATABASE_URL") {
|
||||
Ok(v) if !v.trim().is_empty() => v,
|
||||
_ => return Ok(()),
|
||||
};
|
||||
|
||||
let pool = PgPool::connect(&database_url).await?;
|
||||
|
||||
let has_fingerprint: bool = sqlx::query_scalar(
|
||||
r#"
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema='public' AND table_name='refresh_tokens' AND column_name='token_fingerprint'
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.fetch_one(&pool)
|
||||
.await?;
|
||||
if !has_fingerprint {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let pepper = "test-pepper";
|
||||
let tenant_service = TenantService::new(pool.clone());
|
||||
let auth_service = AuthService::new(pool.clone(), pepper.to_string());
|
||||
|
||||
let tenant = tenant_service
|
||||
.create_tenant(CreateTenantRequest {
|
||||
name: format!("refresh-{}", Uuid::new_v4()),
|
||||
config: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
let user = auth_service
|
||||
.register(
|
||||
tenant.id,
|
||||
CreateUserRequest {
|
||||
email: format!("u-{}@example.com", Uuid::new_v4()),
|
||||
password: "Password12345".to_string(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let login = auth_service
|
||||
.login(
|
||||
tenant.id,
|
||||
LoginRequest {
|
||||
email: user.email.clone(),
|
||||
password: "Password12345".to_string(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let old_refresh = login.refresh_token.clone();
|
||||
|
||||
let refreshed = auth_service
|
||||
.refresh_access_token(old_refresh.clone())
|
||||
.await?;
|
||||
assert_ne!(refreshed.refresh_token, old_refresh);
|
||||
assert!(!refreshed.access_token.is_empty());
|
||||
|
||||
let second = auth_service.refresh_access_token(old_refresh.clone()).await;
|
||||
assert!(second.is_err());
|
||||
|
||||
let invalid = auth_service
|
||||
.refresh_access_token("not-a-real-token".to_string())
|
||||
.await;
|
||||
assert!(invalid.is_err());
|
||||
|
||||
let login2 = auth_service
|
||||
.login(
|
||||
tenant.id,
|
||||
LoginRequest {
|
||||
email: user.email.clone(),
|
||||
password: "Password12345".to_string(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let fp = fingerprint(pepper, &login2.refresh_token);
|
||||
sqlx::query("UPDATE refresh_tokens SET expires_at = NOW() - INTERVAL '1 second' WHERE token_fingerprint = $1")
|
||||
.bind(fp)
|
||||
.execute(&pool)
|
||||
.await?;
|
||||
|
||||
let expired = auth_service
|
||||
.refresh_access_token(login2.refresh_token.clone())
|
||||
.await;
|
||||
assert!(expired.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
92
tests/role_permission_smoke.rs
Normal file
92
tests/role_permission_smoke.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
use iam_service::models::{CreateRoleRequest, CreateTenantRequest, CreateUserRequest};
|
||||
use iam_service::services::{AuthService, AuthorizationService, RoleService, TenantService};
|
||||
use sqlx::PgPool;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[tokio::test]
|
||||
async fn role_permission_grant_and_wildcard_match()
|
||||
-> Result<(), Box<dyn std::error::Error>> {
|
||||
let database_url = match std::env::var("DATABASE_URL") {
|
||||
Ok(v) if !v.trim().is_empty() => v,
|
||||
_ => return Ok(()),
|
||||
};
|
||||
|
||||
let pool = PgPool::connect(&database_url).await?;
|
||||
let tenant_service = TenantService::new(pool.clone());
|
||||
let role_service = RoleService::new(pool.clone());
|
||||
let authz_service = AuthorizationService::new(pool.clone());
|
||||
let auth_service = AuthService::new(pool.clone(), "unused".to_string());
|
||||
|
||||
let tenant = tenant_service
|
||||
.create_tenant(CreateTenantRequest {
|
||||
name: format!("rp-{}", Uuid::new_v4()),
|
||||
config: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
let admin = auth_service
|
||||
.register(
|
||||
tenant.id,
|
||||
CreateUserRequest {
|
||||
email: format!("admin-{}@example.com", Uuid::new_v4()),
|
||||
password: "Password12345".to_string(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let user = auth_service
|
||||
.register(
|
||||
tenant.id,
|
||||
CreateUserRequest {
|
||||
email: format!("user-{}@example.com", Uuid::new_v4()),
|
||||
password: "Password12345".to_string(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let _ = sqlx::query(
|
||||
r#"
|
||||
INSERT INTO permissions (code, description, resource, action)
|
||||
VALUES
|
||||
('cms:article:create', 'Create article', 'article', 'create'),
|
||||
('cms:*:*', 'CMS wildcard', '*', '*')
|
||||
ON CONFLICT (code) DO NOTHING
|
||||
"#,
|
||||
)
|
||||
.execute(&pool)
|
||||
.await?;
|
||||
|
||||
tenant_service
|
||||
.set_enabled_apps(tenant.id, vec!["cms".to_string()], None, admin.id)
|
||||
.await?;
|
||||
|
||||
let role = role_service
|
||||
.create_role(
|
||||
tenant.id,
|
||||
CreateRoleRequest {
|
||||
name: "ContentAdmin".into(),
|
||||
description: None,
|
||||
},
|
||||
admin.id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
role_service
|
||||
.grant_permissions_to_role(
|
||||
tenant.id,
|
||||
role.id,
|
||||
vec!["cms:*:*".to_string()],
|
||||
admin.id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
role_service
|
||||
.grant_role_to_users(tenant.id, role.id, vec![user.id], admin.id)
|
||||
.await?;
|
||||
|
||||
authz_service
|
||||
.require_permission(tenant.id, user.id, "cms:article:create")
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ use sqlx::PgPool;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[tokio::test]
|
||||
async fn set_user_roles_is_idempotent_and_validates_tenant_roles(
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
async fn set_user_roles_is_idempotent_and_validates_tenant_roles()
|
||||
-> Result<(), Box<dyn std::error::Error>> {
|
||||
let database_url = match std::env::var("DATABASE_URL") {
|
||||
Ok(v) if !v.trim().is_empty() => v,
|
||||
_ => return Ok(()),
|
||||
@@ -42,6 +42,7 @@ async fn set_user_roles_is_idempotent_and_validates_tenant_roles(
|
||||
name: "R1".into(),
|
||||
description: Some("role1".into()),
|
||||
},
|
||||
user_id,
|
||||
)
|
||||
.await?;
|
||||
let role2 = role_service
|
||||
@@ -51,6 +52,7 @@ async fn set_user_roles_is_idempotent_and_validates_tenant_roles(
|
||||
name: "R2".into(),
|
||||
description: Some("role2".into()),
|
||||
},
|
||||
user_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -80,6 +82,7 @@ async fn set_user_roles_is_idempotent_and_validates_tenant_roles(
|
||||
name: "Other".into(),
|
||||
description: None,
|
||||
},
|
||||
user_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user