style: cargo fmt across Phase 3.5 changes
Pure formatting pass — no behavior changes. Catches the line-wrapping drift across the new authz / api_keys / middleware / handler edits that piled up during the implementation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -383,8 +383,9 @@ impl TryFrom<AdminUserRecord> for AdminUserRow {
|
||||
id: r.id.into(),
|
||||
username: r.username,
|
||||
is_active: r.is_active,
|
||||
instance_role: InstanceRole::from_db_str(&r.instance_role)
|
||||
.ok_or(AdminUserRepositoryError::InvalidInstanceRole(r.instance_role))?,
|
||||
instance_role: InstanceRole::from_db_str(&r.instance_role).ok_or(
|
||||
AdminUserRepositoryError::InvalidInstanceRole(r.instance_role),
|
||||
)?,
|
||||
email: r.email,
|
||||
created_at: r.created_at,
|
||||
updated_at: r.updated_at,
|
||||
@@ -410,8 +411,9 @@ impl TryFrom<AdminCredsRecord> for AdminUserCredentials {
|
||||
username: r.username,
|
||||
password_hash: r.password_hash,
|
||||
is_active: r.is_active,
|
||||
instance_role: InstanceRole::from_db_str(&r.instance_role)
|
||||
.ok_or(AdminUserRepositoryError::InvalidInstanceRole(r.instance_role))?,
|
||||
instance_role: InstanceRole::from_db_str(&r.instance_role).ok_or(
|
||||
AdminUserRepositoryError::InvalidInstanceRole(r.instance_role),
|
||||
)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,8 +162,7 @@ async fn create_admin(
|
||||
// owner — admin cannot self-elevate (or elevate someone else)
|
||||
// beyond their own ceiling. Owner-creation by env-var bootstrap
|
||||
// bypasses this path.
|
||||
if input.instance_role == InstanceRole::Owner
|
||||
&& principal.instance_role != InstanceRole::Owner
|
||||
if input.instance_role == InstanceRole::Owner && principal.instance_role != InstanceRole::Owner
|
||||
{
|
||||
return Err(AdminApiError::CannotEscalate);
|
||||
}
|
||||
|
||||
@@ -112,10 +112,8 @@ pub trait ApiKeyRepository: Send + Sync {
|
||||
/// into `set_active(false)` so deactivation invalidates both
|
||||
/// sessions (already done by `AdminSessionRepository::delete_for_user`)
|
||||
/// and bearer keys at the same moment.
|
||||
async fn expire_all_for_user(
|
||||
&self,
|
||||
user_id: AdminUserId,
|
||||
) -> Result<u64, ApiKeyRepositoryError>;
|
||||
async fn expire_all_for_user(&self, user_id: AdminUserId)
|
||||
-> Result<u64, ApiKeyRepositoryError>;
|
||||
}
|
||||
|
||||
pub struct PostgresApiKeyRepository {
|
||||
@@ -132,7 +130,8 @@ impl PostgresApiKeyRepository {
|
||||
#[async_trait]
|
||||
impl ApiKeyRepository for PostgresApiKeyRepository {
|
||||
async fn create(&self, key: NewApiKey) -> Result<ApiKeyRow, ApiKeyRepositoryError> {
|
||||
let scope_strings: Vec<String> = key.scopes.iter().map(|s| s.as_str().to_string()).collect();
|
||||
let scope_strings: Vec<String> =
|
||||
key.scopes.iter().map(|s| s.as_str().to_string()).collect();
|
||||
let row = sqlx::query_as::<_, ApiKeyRecord>(
|
||||
"INSERT INTO api_keys \
|
||||
(user_id, hash, prefix, name, scopes, app_id, expires_at) \
|
||||
|
||||
@@ -19,10 +19,7 @@ pub enum AppMembersRepositoryError {
|
||||
Db(#[from] sqlx::Error),
|
||||
|
||||
#[error("membership row not found: app={app_id}, user={user_id}")]
|
||||
NotFound {
|
||||
app_id: AppId,
|
||||
user_id: AdminUserId,
|
||||
},
|
||||
NotFound { app_id: AppId, user_id: AdminUserId },
|
||||
|
||||
#[error("invalid app_role stored in DB: {0}")]
|
||||
InvalidRole(String),
|
||||
|
||||
@@ -27,10 +27,7 @@ pub trait AppRepository: Send + Sync {
|
||||
async fn list(&self) -> Result<Vec<App>, ScriptRepositoryError>;
|
||||
/// Only apps the user has an `app_members` row for. Drives the
|
||||
/// membership-filtered `GET /admin/apps` for `member` callers.
|
||||
async fn list_for_user(
|
||||
&self,
|
||||
user_id: AdminUserId,
|
||||
) -> Result<Vec<App>, ScriptRepositoryError>;
|
||||
async fn list_for_user(&self, user_id: AdminUserId) -> Result<Vec<App>, ScriptRepositoryError>;
|
||||
async fn get_by_id(&self, id: AppId) -> Result<Option<App>, ScriptRepositoryError>;
|
||||
async fn get_by_slug(&self, slug: &str) -> Result<Option<App>, ScriptRepositoryError>;
|
||||
async fn get_by_slug_or_history(
|
||||
@@ -100,10 +97,7 @@ impl AppRepository for PostgresAppRepository {
|
||||
Ok(rows.into_iter().map(Into::into).collect())
|
||||
}
|
||||
|
||||
async fn list_for_user(
|
||||
&self,
|
||||
user_id: AdminUserId,
|
||||
) -> Result<Vec<App>, ScriptRepositoryError> {
|
||||
async fn list_for_user(&self, user_id: AdminUserId) -> Result<Vec<App>, ScriptRepositoryError> {
|
||||
let rows = sqlx::query_as::<_, AppRow>(
|
||||
"SELECT a.id, a.slug, a.name, a.description, a.created_at, a.updated_at \
|
||||
FROM apps a \
|
||||
|
||||
@@ -223,6 +223,9 @@ mod tests {
|
||||
let b = generate_api_key().expect("mint b");
|
||||
assert_ne!(a.raw, b.raw);
|
||||
assert_ne!(a.hash, b.hash);
|
||||
assert_ne!(a.prefix, b.prefix, "32 random bytes → prefix collision is negligible");
|
||||
assert_ne!(
|
||||
a.prefix, b.prefix,
|
||||
"32 random bytes → prefix collision is negligible"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,7 +160,10 @@ async fn logout(State(state): State<AuthState>, req: Request<Body>) -> Response
|
||||
(StatusCode::NO_CONTENT, headers).into_response()
|
||||
}
|
||||
|
||||
async fn me(State(state): State<AuthState>, Extension(principal): Extension<Principal>) -> Response {
|
||||
async fn me(
|
||||
State(state): State<AuthState>,
|
||||
Extension(principal): Extension<Principal>,
|
||||
) -> Response {
|
||||
// /me consumes the resolved Principal directly; we re-fetch the
|
||||
// user row only to surface a fresh username (it can change via
|
||||
// PATCH while a session/key is still valid).
|
||||
|
||||
@@ -272,7 +272,9 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn populated_db_is_noop() {
|
||||
let repo = InMemoryRepo::default();
|
||||
repo.create("seeded", "x", InstanceRole::Owner).await.unwrap();
|
||||
repo.create("seeded", "x", InstanceRole::Owner)
|
||||
.await
|
||||
.unwrap();
|
||||
let env = BootstrapEnv {
|
||||
username: Some("alice".into()),
|
||||
password: Some("supersecret".into()),
|
||||
|
||||
@@ -96,11 +96,7 @@ pub async fn require_authenticated(
|
||||
/// `require_admin` keeps working without an immediate rename. New
|
||||
/// wiring should call `require_authenticated`.
|
||||
#[deprecated(note = "renamed to require_authenticated")]
|
||||
pub async fn require_admin(
|
||||
state: State<AuthState>,
|
||||
req: Request<Body>,
|
||||
next: Next,
|
||||
) -> Response {
|
||||
pub async fn require_admin(state: State<AuthState>, req: Request<Body>, next: Next) -> Response {
|
||||
require_authenticated(state, req, next).await
|
||||
}
|
||||
|
||||
@@ -164,10 +160,7 @@ async fn verify_session(
|
||||
/// against the full `rest`. At most one match is expected; multiple
|
||||
/// candidates with the same prefix is statistically negligible but
|
||||
/// handled correctly (verify each, take the first match).
|
||||
async fn verify_api_key(
|
||||
state: &AuthState,
|
||||
rest: &str,
|
||||
) -> Result<Option<Principal>, InternalError> {
|
||||
async fn verify_api_key(state: &AuthState, rest: &str) -> Result<Option<Principal>, InternalError> {
|
||||
if rest.len() <= API_KEY_PREFIX_LEN {
|
||||
return Ok(None);
|
||||
}
|
||||
@@ -224,7 +217,10 @@ async fn username_for(state: &AuthState, id: AdminUserId) -> Option<String> {
|
||||
Ok(Some(u)) => Some(u.username),
|
||||
Ok(None) => None,
|
||||
Err(err) => {
|
||||
tracing::warn!(?err, "username lookup for AuthedAdmin failed; skipping legacy ext");
|
||||
tracing::warn!(
|
||||
?err,
|
||||
"username lookup for AuthedAdmin failed; skipping legacy ext"
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,9 +65,9 @@ impl Capability {
|
||||
#[must_use]
|
||||
pub const fn app_id(self) -> Option<AppId> {
|
||||
match self {
|
||||
Self::InstanceCreateApp
|
||||
| Self::InstanceManageUsers
|
||||
| Self::InstanceManageSettings => None,
|
||||
Self::InstanceCreateApp | Self::InstanceManageUsers | Self::InstanceManageSettings => {
|
||||
None
|
||||
}
|
||||
Self::AppRead(id)
|
||||
| Self::AppWriteScript(id)
|
||||
| Self::AppWriteRoute(id)
|
||||
@@ -85,9 +85,9 @@ impl Capability {
|
||||
#[must_use]
|
||||
pub const fn required_scope(self) -> Scope {
|
||||
match self {
|
||||
Self::InstanceCreateApp
|
||||
| Self::InstanceManageUsers
|
||||
| Self::InstanceManageSettings => Scope::InstanceAdmin,
|
||||
Self::InstanceCreateApp | Self::InstanceManageUsers | Self::InstanceManageSettings => {
|
||||
Scope::InstanceAdmin
|
||||
}
|
||||
Self::AppRead(_) => Scope::ScriptRead,
|
||||
Self::AppWriteScript(_) => Scope::ScriptWrite,
|
||||
Self::AppWriteRoute(_) => Scope::RouteWrite,
|
||||
@@ -314,7 +314,12 @@ mod tests {
|
||||
user_id: UserId,
|
||||
app_id: AppId,
|
||||
) -> Result<Option<AppRole>, AuthzError> {
|
||||
Ok(self.memberships.lock().await.get(&(user_id, app_id)).copied())
|
||||
Ok(self
|
||||
.memberships
|
||||
.lock()
|
||||
.await
|
||||
.get(&(user_id, app_id))
|
||||
.copied())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,25 +366,35 @@ mod tests {
|
||||
Decision::Allow,
|
||||
);
|
||||
assert_eq!(
|
||||
can(&repo, &p, Capability::InstanceManageUsers).await.unwrap(),
|
||||
can(&repo, &p, Capability::InstanceManageUsers)
|
||||
.await
|
||||
.unwrap(),
|
||||
Decision::Allow,
|
||||
);
|
||||
assert_eq!(
|
||||
can(&repo, &p, Capability::InstanceManageSettings).await.unwrap(),
|
||||
can(&repo, &p, Capability::InstanceManageSettings)
|
||||
.await
|
||||
.unwrap(),
|
||||
Decision::Deny,
|
||||
);
|
||||
// Editor-like grants succeed
|
||||
assert_eq!(
|
||||
can(&repo, &p, Capability::AppWriteScript(app)).await.unwrap(),
|
||||
can(&repo, &p, Capability::AppWriteScript(app))
|
||||
.await
|
||||
.unwrap(),
|
||||
Decision::Allow,
|
||||
);
|
||||
assert_eq!(
|
||||
can(&repo, &p, Capability::AppWriteRoute(app)).await.unwrap(),
|
||||
can(&repo, &p, Capability::AppWriteRoute(app))
|
||||
.await
|
||||
.unwrap(),
|
||||
Decision::Allow,
|
||||
);
|
||||
// App-admin grants do not
|
||||
assert_eq!(
|
||||
can(&repo, &p, Capability::AppManageDomains(app)).await.unwrap(),
|
||||
can(&repo, &p, Capability::AppManageDomains(app))
|
||||
.await
|
||||
.unwrap(),
|
||||
Decision::Deny,
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -418,10 +433,18 @@ mod tests {
|
||||
let app = AppId::new();
|
||||
repo.grant(p.user_id, app, AppRole::Viewer).await;
|
||||
|
||||
assert!(can(&repo, &p, Capability::AppRead(app)).await.unwrap().is_allow());
|
||||
assert!(can(&repo, &p, Capability::AppLogRead(app)).await.unwrap().is_allow());
|
||||
assert!(can(&repo, &p, Capability::AppRead(app))
|
||||
.await
|
||||
.unwrap()
|
||||
.is_allow());
|
||||
assert!(can(&repo, &p, Capability::AppLogRead(app))
|
||||
.await
|
||||
.unwrap()
|
||||
.is_allow());
|
||||
assert_eq!(
|
||||
can(&repo, &p, Capability::AppWriteScript(app)).await.unwrap(),
|
||||
can(&repo, &p, Capability::AppWriteScript(app))
|
||||
.await
|
||||
.unwrap(),
|
||||
Decision::Deny
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -437,8 +460,14 @@ mod tests {
|
||||
let app = AppId::new();
|
||||
repo.grant(p.user_id, app, AppRole::Editor).await;
|
||||
|
||||
assert!(can(&repo, &p, Capability::AppWriteScript(app)).await.unwrap().is_allow());
|
||||
assert!(can(&repo, &p, Capability::AppWriteRoute(app)).await.unwrap().is_allow());
|
||||
assert!(can(&repo, &p, Capability::AppWriteScript(app))
|
||||
.await
|
||||
.unwrap()
|
||||
.is_allow());
|
||||
assert!(can(&repo, &p, Capability::AppWriteRoute(app))
|
||||
.await
|
||||
.unwrap()
|
||||
.is_allow());
|
||||
assert_eq!(
|
||||
can(&repo, &p, Capability::AppAdmin(app)).await.unwrap(),
|
||||
Decision::Deny
|
||||
@@ -452,12 +481,20 @@ mod tests {
|
||||
let app = AppId::new();
|
||||
repo.grant(p.user_id, app, AppRole::AppAdmin).await;
|
||||
|
||||
assert!(can(&repo, &p, Capability::AppAdmin(app)).await.unwrap().is_allow());
|
||||
assert!(can(&repo, &p, Capability::AppManageDomains(app)).await.unwrap().is_allow());
|
||||
assert!(can(&repo, &p, Capability::AppAdmin(app))
|
||||
.await
|
||||
.unwrap()
|
||||
.is_allow());
|
||||
assert!(can(&repo, &p, Capability::AppManageDomains(app))
|
||||
.await
|
||||
.unwrap()
|
||||
.is_allow());
|
||||
// Membership in App A does NOT grant access to App B
|
||||
let other_app = AppId::new();
|
||||
assert_eq!(
|
||||
can(&repo, &p, Capability::AppAdmin(other_app)).await.unwrap(),
|
||||
can(&repo, &p, Capability::AppAdmin(other_app))
|
||||
.await
|
||||
.unwrap(),
|
||||
Decision::Deny
|
||||
);
|
||||
}
|
||||
@@ -473,9 +510,14 @@ mod tests {
|
||||
scopes: Some(vec![Scope::ScriptRead]),
|
||||
app_binding: None,
|
||||
};
|
||||
assert!(can(&repo, &p, Capability::AppRead(app)).await.unwrap().is_allow());
|
||||
assert!(can(&repo, &p, Capability::AppRead(app))
|
||||
.await
|
||||
.unwrap()
|
||||
.is_allow());
|
||||
assert_eq!(
|
||||
can(&repo, &p, Capability::AppWriteScript(app)).await.unwrap(),
|
||||
can(&repo, &p, Capability::AppWriteScript(app))
|
||||
.await
|
||||
.unwrap(),
|
||||
Decision::Deny
|
||||
);
|
||||
// Even though the user is owner — the key's scope set is the
|
||||
@@ -502,7 +544,9 @@ mod tests {
|
||||
.unwrap()
|
||||
.is_allow());
|
||||
assert_eq!(
|
||||
can(&repo, &p, Capability::AppWriteScript(other_app)).await.unwrap(),
|
||||
can(&repo, &p, Capability::AppWriteScript(other_app))
|
||||
.await
|
||||
.unwrap(),
|
||||
Decision::Deny
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user