Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions backend/core/src/types/entity_aliases.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ pub type ECacheRole = cache_role::Entity;
pub type ECachedPath = cached_path::Entity;
pub type ECacheUpstream = cache_upstream::Entity;
pub type ECacheUser = cache_user::Entity;
pub type ECliDeviceAuthorization = cli_device_authorization::Entity;
pub type ECommit = commit::Entity;
pub type EDerivation = derivation::Entity;
pub type EDerivationDependency = derivation_dependency::Entity;
Expand Down Expand Up @@ -83,6 +84,7 @@ pub type MCacheRole = cache_role::Model;
pub type MCachedPath = cached_path::Model;
pub type MCacheUpstream = cache_upstream::Model;
pub type MCacheUser = cache_user::Model;
pub type MCliDeviceAuthorization = cli_device_authorization::Model;
pub type MCommit = commit::Model;
pub type MDerivation = derivation::Model;
pub type MDerivationDependency = derivation_dependency::Model;
Expand Down Expand Up @@ -123,6 +125,7 @@ pub type ACacheRole = cache_role::ActiveModel;
pub type ACachedPath = cached_path::ActiveModel;
pub type ACacheUpstream = cache_upstream::ActiveModel;
pub type ACacheUser = cache_user::ActiveModel;
pub type ACliDeviceAuthorization = cli_device_authorization::ActiveModel;
pub type ACommit = commit::ActiveModel;
pub type ADerivation = derivation::ActiveModel;
pub type ADerivationDependency = derivation_dependency::ActiveModel;
Expand Down Expand Up @@ -163,6 +166,7 @@ pub type CCacheRole = cache_role::Column;
pub type CCachedPath = cached_path::Column;
pub type CCacheUpstream = cache_upstream::Column;
pub type CCacheUser = cache_user::Column;
pub type CCliDeviceAuthorization = cli_device_authorization::Column;
pub type CCommit = commit::Column;
pub type CDerivation = derivation::Column;
pub type CDerivationDependency = derivation_dependency::Column;
Expand Down
58 changes: 58 additions & 0 deletions backend/entity/src/cli_device_authorization.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* SPDX-FileCopyrightText: 2026 Wavelens GmbH <info@wavelens.io>
*
* SPDX-License-Identifier: AGPL-3.0-only
*/

use chrono::NaiveDateTime;
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};

use crate::ids::{CliDeviceAuthorizationId, UserId};

#[derive(Clone, Default, PartialEq, DeriveEntityModel, Deserialize, Serialize)]
#[sea_orm(table_name = "cli_device_authorization")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: CliDeviceAuthorizationId,
pub device_code_hash: String,
pub user_code: String,
pub user_id: Option<UserId>,
pub token: Option<String>,
pub denied_at: Option<NaiveDateTime>,
pub authorized_at: Option<NaiveDateTime>,
pub created_at: NaiveDateTime,
pub expires_at: NaiveDateTime,
pub user_agent: Option<String>,
pub ip: Option<String>,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::UserId",
to = "super::user::Column::Id"
)]
User,
}

impl std::fmt::Debug for Model {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CliDeviceAuthorization")
.field("id", &self.id)
.field("device_code_hash", &"[redacted]")
.field("user_code", &self.user_code)
.field("user_id", &self.user_id)
.field("token", &self.token.as_ref().map(|_| "[redacted]"))
.field("denied_at", &self.denied_at)
.field("authorized_at", &self.authorized_at)
.field("created_at", &self.created_at)
.field("expires_at", &self.expires_at)
.field("user_agent", &self.user_agent)
.field("ip", &self.ip)
.finish()
}
}

impl ActiveModelBehavior for ActiveModel {}
1 change: 1 addition & 0 deletions backend/entity/src/ids.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ id_newtype!(SessionId);
id_newtype!(UploadSessionId);
id_newtype!(AuditLogId);
id_newtype!(WorkerRegistrationId);
id_newtype!(CliDeviceAuthorizationId);

#[cfg(test)]
mod tests {
Expand Down
1 change: 1 addition & 0 deletions backend/entity/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub mod cache_upstream;
pub mod cache_user;
pub mod cached_path;
pub mod cached_path_signature;
pub mod cli_device_authorization;
pub mod commit;
pub mod derivation;
pub mod derivation_dependency;
Expand Down
2 changes: 2 additions & 0 deletions backend/migration/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ mod m20260525_000004_evaluation_check_run_ids;
mod m20260527_000000_evaluation_source_comment;
mod m20260527_000001_add_allowed_ips_to_api;
mod m20260527_000002_add_allowed_ips_to_integration;
mod m20260528_000000_create_table_cli_device_authorization;

pub struct Migrator;

Expand Down Expand Up @@ -241,6 +242,7 @@ impl MigratorTrait for Migrator {
Box::new(m20260527_000000_evaluation_source_comment::Migration),
Box::new(m20260527_000001_add_allowed_ips_to_api::Migration),
Box::new(m20260527_000002_add_allowed_ips_to_integration::Migration),
Box::new(m20260528_000000_create_table_cli_device_authorization::Migration),
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
* SPDX-FileCopyrightText: 2026 Wavelens GmbH <info@wavelens.io>
*
* SPDX-License-Identifier: AGPL-3.0-only
*/

//! Storage for the OAuth 2.0 Device Authorization Grant used by `gradient login`.
//!
//! One row per `gradient login` invocation. The CLI polls `/auth/cli/poll` with
//! `device_code` (stored hashed) until the browser-side user clicks Authorize at
//! `/account/cli-authorize?code=<user_code>`, at which point `user_id` and
//! `token` are populated and subsequent polls return the token.

use sea_orm_migration::prelude::*;

#[derive(DeriveMigrationName)]
pub struct Migration;

#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(CliDeviceAuthorization::Table)
.if_not_exists()
.col(
ColumnDef::new(CliDeviceAuthorization::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(
ColumnDef::new(CliDeviceAuthorization::DeviceCodeHash)
.string()
.not_null()
.unique_key(),
)
.col(
ColumnDef::new(CliDeviceAuthorization::UserCode)
.string()
.not_null()
.unique_key(),
)
.col(ColumnDef::new(CliDeviceAuthorization::UserId).uuid().null())
.col(ColumnDef::new(CliDeviceAuthorization::Token).text().null())
.col(
ColumnDef::new(CliDeviceAuthorization::DeniedAt)
.date_time()
.null(),
)
.col(
ColumnDef::new(CliDeviceAuthorization::AuthorizedAt)
.date_time()
.null(),
)
.col(
ColumnDef::new(CliDeviceAuthorization::CreatedAt)
.date_time()
.not_null(),
)
.col(
ColumnDef::new(CliDeviceAuthorization::ExpiresAt)
.date_time()
.not_null(),
)
.col(
ColumnDef::new(CliDeviceAuthorization::UserAgent)
.text()
.null(),
)
.col(ColumnDef::new(CliDeviceAuthorization::Ip).string().null())
.foreign_key(
ForeignKey::create()
.name("fk-cli-device-auth-user")
.from(
CliDeviceAuthorization::Table,
CliDeviceAuthorization::UserId,
)
.to(User::Table, User::Id)
.on_delete(ForeignKeyAction::Cascade)
.on_update(ForeignKeyAction::Cascade),
)
.to_owned(),
)
.await?;

manager
.create_index(
Index::create()
.if_not_exists()
.name("idx_cli_device_auth_expires_at")
.table(CliDeviceAuthorization::Table)
.col(CliDeviceAuthorization::ExpiresAt)
.to_owned(),
)
.await
}

async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(
Table::drop()
.table(CliDeviceAuthorization::Table)
.to_owned(),
)
.await
}
}

#[derive(DeriveIden)]
enum CliDeviceAuthorization {
Table,
Id,
DeviceCodeHash,
UserCode,
UserId,
Token,
DeniedAt,
AuthorizedAt,
CreatedAt,
ExpiresAt,
UserAgent,
Ip,
}

#[derive(DeriveIden)]
enum User {
Table,
Id,
}
3 changes: 3 additions & 0 deletions backend/web/src/audit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ pub mod events {
pub const API_KEY_DELETE: &str = "api_key.delete";
pub const SESSION_REVOKE: &str = "session.revoke";
pub const AUTH_DENY: &str = "auth.deny";
pub const CLI_DEVICE_START: &str = "cli.device.start";
pub const CLI_DEVICE_AUTHORIZE: &str = "cli.device.authorize";
pub const CLI_DEVICE_DENY: &str = "cli.device.deny";
pub const ORG_DELETE: &str = "organization.delete";
pub const ORG_MEMBER_ADD: &str = "organization.member.add";
pub const ORG_MEMBER_REMOVE: &str = "organization.member.remove";
Expand Down
Loading
Loading