feat: centralize auth REST contracts
This commit is contained in:
@@ -1,54 +1,11 @@
|
||||
use axum::http::{HeaderMap, header};
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use uuid::Uuid;
|
||||
pub use workspace_api::{ActorAuthMethod, AuthPublicConfig, AuthenticatedUser, RequestActor};
|
||||
|
||||
use crate::{Error, Result, store::ControlPlaneStore};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct AuthPublicConfig {
|
||||
pub rp_id: String,
|
||||
pub origin: String,
|
||||
pub public_base_url: String,
|
||||
pub cookie_name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct RequestActor {
|
||||
pub user_id: String,
|
||||
pub account_id: String,
|
||||
pub handle: String,
|
||||
pub display_name: String,
|
||||
pub auth_method: ActorAuthMethod,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ActorAuthMethod {
|
||||
BrowserSession,
|
||||
ApiToken,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct AuthenticatedUser {
|
||||
pub user_id: String,
|
||||
pub account_id: String,
|
||||
pub handle: String,
|
||||
pub display_name: String,
|
||||
}
|
||||
|
||||
impl RequestActor {
|
||||
pub fn user(&self) -> AuthenticatedUser {
|
||||
AuthenticatedUser {
|
||||
user_id: self.user_id.clone(),
|
||||
account_id: self.account_id.clone(),
|
||||
handle: self.handle.clone(),
|
||||
display_name: self.display_name.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn normalize_handle(handle: &str) -> Result<String> {
|
||||
let normalized = handle.trim().to_ascii_lowercase();
|
||||
let valid = !normalized.is_empty()
|
||||
|
||||
@@ -41,9 +41,7 @@ use tower::ServiceExt;
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
use webauthn_rs::prelude::{
|
||||
CreationChallengeResponse, Passkey, PasskeyAuthentication, PasskeyRegistration,
|
||||
PublicKeyCredential, RegisterPublicKeyCredential, RequestChallengeResponse, Webauthn,
|
||||
WebauthnBuilder,
|
||||
Passkey, PasskeyAuthentication, PasskeyRegistration, Webauthn, WebauthnBuilder,
|
||||
};
|
||||
use workdir::http::{
|
||||
WorkdirSessionOperation, WorkdirSessionOperationResult, WorkdirTransportError,
|
||||
@@ -58,19 +56,26 @@ use worker::feature::builtin::{WorkerObservationSubject, WorkerObservationSubjec
|
||||
use worker_runtime::resource::{BackendResourceError, BackendResourceFetchRequest};
|
||||
use worker_runtime::worker_backend::{ProfileRuntimeWorkerFactory, WorkerRuntimeExecutionBackend};
|
||||
use workspace_api::{
|
||||
BrowserCreateWorkerResponse, BrowserWorkspaceOrchestratorResponse, CreateRemoteRuntimeRequest,
|
||||
CreateRepositorySshCredentialRequest, CreateWorkspaceRepositoryRequest,
|
||||
CreateWorkspaceRepositoryResponse, CreateWorkspaceWorkerRequest,
|
||||
CreateWorkspaceWorkerTicketAssignmentRequest, DeleteRepositorySshCredentialRequest,
|
||||
DeleteRepositorySshHostTrustRequest, MemoryDocumentResponse, MemoryStagingListResponse,
|
||||
ObjectiveCreateRequest, ObjectiveEditRequest, ObjectiveLinkTicketRequest,
|
||||
ObjectiveStateRequest, ProfileSettingsResponse, PutRepositorySshHostTrustRequest,
|
||||
ActorAuthMethod, AuthBootstrapUserRequest, AuthPublicConfig, AuthUserResponse,
|
||||
AuthenticatedUser, BrowserCreateWorkerResponse, BrowserWorkspaceOrchestratorResponse,
|
||||
CreateRemoteRuntimeRequest, CreateRepositorySshCredentialRequest,
|
||||
CreateWorkspaceRepositoryRequest, CreateWorkspaceRepositoryResponse,
|
||||
CreateWorkspaceWorkerRequest, CreateWorkspaceWorkerTicketAssignmentRequest,
|
||||
DeleteRepositorySshCredentialRequest, DeleteRepositorySshHostTrustRequest,
|
||||
DeviceAccessTokenType, DeviceLoginApprovalStatus, DeviceLoginApproveRequest,
|
||||
DeviceLoginApproveResponse, DeviceLoginPollRequest, DeviceLoginPollResponse,
|
||||
DeviceLoginPollStatus, DeviceLoginStartRequest, DeviceLoginStartResponse, LogoutResponse,
|
||||
LogoutStatus, MemoryDocumentResponse, MemoryStagingListResponse, ObjectiveCreateRequest,
|
||||
ObjectiveEditRequest, ObjectiveLinkTicketRequest, ObjectiveStateRequest,
|
||||
PasskeyLoginCompleteRequest, PasskeyLoginOptionsRequest, PasskeyLoginOptionsResponse,
|
||||
PasskeyRegistrationCompleteRequest, PasskeyRegistrationOptionsRequest,
|
||||
PasskeyRegistrationOptionsResponse, ProfileSettingsResponse, PutRepositorySshHostTrustRequest,
|
||||
RepositoryAccessProjection, RepositoryDetailResponse, RepositoryListResponse,
|
||||
RepositoryLogResponse, RepositorySshCredential, RepositorySshHostTrust,
|
||||
RepositoryLogResponse, RepositorySshCredential, RepositorySshHostTrust, RequestActor,
|
||||
RotateRepositorySshCredentialRequest, RuntimeConnectionTestResponse, RuntimeManagementSummary,
|
||||
TICKET_ORCHESTRATION_PLANS_QUERY_PATH, TICKET_RELATIONS_QUERY_PATH,
|
||||
UpdateWorkspaceMetadataRequest, WorkerLaunchOptionsResponse, WorkerLaunchProfileCandidate,
|
||||
WorkerLaunchRuntimeOption, WorkerLaunchWorkerSummary,
|
||||
UpdateWorkspaceMetadataRequest, WhoamiResponse, WorkerLaunchOptionsResponse,
|
||||
WorkerLaunchProfileCandidate, WorkerLaunchRuntimeOption, WorkerLaunchWorkerSummary,
|
||||
WorkingDirectoryCreateRequest as BrowserWorkingDirectoryCreateRequest,
|
||||
WorkingDirectoryCreateResponse as BrowserWorkingDirectoryCreateResponse,
|
||||
WorkingDirectoryDetailResponse as BrowserWorkingDirectoryDetailResponse,
|
||||
@@ -85,9 +90,9 @@ use workspace_api::{
|
||||
};
|
||||
|
||||
use crate::auth::{
|
||||
ActorAuthMethod, AuthPublicConfig, AuthenticatedUser, RequestActor, SessionCookiePolicy,
|
||||
auth_error, is_expired, mint_secret, new_id, new_user_code, normalize_handle, parse_cookie,
|
||||
resolve_request_actor, rfc3339_after, session_set_cookie, token_hash,
|
||||
SessionCookiePolicy, auth_error, is_expired, mint_secret, new_id, new_user_code,
|
||||
normalize_handle, parse_cookie, resolve_request_actor, rfc3339_after, session_set_cookie,
|
||||
token_hash,
|
||||
};
|
||||
use crate::authority::{
|
||||
MemoryAuthority, ObjectiveAuthority, ObjectiveCreateInput, ObjectiveEditInput,
|
||||
@@ -11477,118 +11482,6 @@ async fn scoped_list_host_workers(
|
||||
list_host_workers(State(api), AxumPath(path.host_id)).await
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct AuthBootstrapUserRequest {
|
||||
handle: String,
|
||||
#[serde(default)]
|
||||
display_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct AuthUserResponse {
|
||||
user: AuthenticatedUser,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct PasskeyRegistrationOptionsRequest {
|
||||
handle: String,
|
||||
#[serde(default)]
|
||||
display_name: Option<String>,
|
||||
#[serde(default)]
|
||||
browser_origin: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct PasskeyRegistrationOptionsResponse {
|
||||
challenge_id: String,
|
||||
public_key: CreationChallengeResponse,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct PasskeyRegistrationCompleteRequest {
|
||||
challenge_id: String,
|
||||
credential: RegisterPublicKeyCredential,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct PasskeyLoginOptionsRequest {
|
||||
#[serde(default)]
|
||||
handle: Option<String>,
|
||||
#[serde(default)]
|
||||
browser_origin: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct PasskeyLoginOptionsResponse {
|
||||
challenge_id: String,
|
||||
public_key: RequestChallengeResponse,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct PasskeyLoginCompleteRequest {
|
||||
challenge_id: String,
|
||||
credential: PublicKeyCredential,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct DeviceLoginStartRequest {
|
||||
#[serde(default)]
|
||||
client_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct DeviceLoginStartResponse {
|
||||
device_code: String,
|
||||
user_code: String,
|
||||
verification_uri: String,
|
||||
verification_uri_complete: String,
|
||||
expires_in: u64,
|
||||
interval: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct DeviceLoginApproveRequest {
|
||||
user_code: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct DeviceLoginApproveResponse {
|
||||
status: String,
|
||||
user: AuthenticatedUser,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct DeviceLoginPollRequest {
|
||||
device_code: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct DeviceLoginPollResponse {
|
||||
status: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
access_token: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
token_type: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct WhoamiResponse {
|
||||
actor: Option<RequestActor>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct LogoutResponse {
|
||||
status: String,
|
||||
}
|
||||
|
||||
async fn get_auth_config(State(api): State<ServerAuthApi>) -> ApiResult<Json<AuthPublicConfig>> {
|
||||
Ok(Json(auth_public_config(&api.config)))
|
||||
}
|
||||
@@ -11993,7 +11886,7 @@ async fn post_device_login_approve(
|
||||
.into());
|
||||
}
|
||||
Ok(Json(DeviceLoginApproveResponse {
|
||||
status: "approved".to_string(),
|
||||
status: DeviceLoginApprovalStatus::Approved,
|
||||
user: actor.user(),
|
||||
}))
|
||||
}
|
||||
@@ -12010,14 +11903,14 @@ async fn post_device_login_poll(
|
||||
};
|
||||
if is_expired(&flow.expires_at) {
|
||||
return Ok(Json(DeviceLoginPollResponse {
|
||||
status: "expired".to_string(),
|
||||
status: DeviceLoginPollStatus::Expired,
|
||||
access_token: None,
|
||||
token_type: None,
|
||||
}));
|
||||
}
|
||||
if flow.approved_at.is_none() {
|
||||
return Ok(Json(DeviceLoginPollResponse {
|
||||
status: "pending".to_string(),
|
||||
status: DeviceLoginPollStatus::Pending,
|
||||
access_token: None,
|
||||
token_type: None,
|
||||
}));
|
||||
@@ -12027,15 +11920,15 @@ async fn post_device_login_poll(
|
||||
.consume_device_login_token(&request.device_code, &crate::auth::now_rfc3339())?
|
||||
else {
|
||||
return Ok(Json(DeviceLoginPollResponse {
|
||||
status: "consumed".to_string(),
|
||||
status: DeviceLoginPollStatus::Consumed,
|
||||
access_token: None,
|
||||
token_type: None,
|
||||
}));
|
||||
};
|
||||
Ok(Json(DeviceLoginPollResponse {
|
||||
status: "approved".to_string(),
|
||||
status: DeviceLoginPollStatus::Approved,
|
||||
access_token: consumed.issued_access_token,
|
||||
token_type: Some("Bearer".to_string()),
|
||||
token_type: Some(DeviceAccessTokenType::Bearer),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -12073,7 +11966,7 @@ async fn post_auth_logout(
|
||||
Ok((
|
||||
response_headers,
|
||||
Json(LogoutResponse {
|
||||
status: "logged_out".to_string(),
|
||||
status: LogoutStatus::LoggedOut,
|
||||
}),
|
||||
)
|
||||
.into_response())
|
||||
|
||||
Reference in New Issue
Block a user