diff --git a/src/declarations/observatory/observatory.did.d.ts b/src/declarations/observatory/observatory.did.d.ts index 1c94f148bf..26d7fcf636 100644 --- a/src/declarations/observatory/observatory.did.d.ts +++ b/src/declarations/observatory/observatory.did.d.ts @@ -106,7 +106,7 @@ export interface OpenIdCertificate { created_at: bigint; version: [] | [bigint]; } -export type OpenIdProvider = { Google: null } | { GitHubAuth: null }; +export type OpenIdProvider = { GitHubActions: null } | { Google: null } | { GitHubAuth: null }; export interface RateConfig { max_tokens: bigint; time_per_token_ns: bigint; diff --git a/src/declarations/observatory/observatory.factory.certified.did.js b/src/declarations/observatory/observatory.factory.certified.did.js index 426638d956..af9d86bc0c 100644 --- a/src/declarations/observatory/observatory.factory.certified.did.js +++ b/src/declarations/observatory/observatory.factory.certified.did.js @@ -21,6 +21,7 @@ export const idlFactory = ({ IDL }) => { failed: IDL.Nat64 }); const OpenIdProvider = IDL.Variant({ + GitHubActions: IDL.Null, Google: IDL.Null, GitHubAuth: IDL.Null }); diff --git a/src/declarations/observatory/observatory.factory.did.js b/src/declarations/observatory/observatory.factory.did.js index 4b2ad5081f..4a5a7bbc23 100644 --- a/src/declarations/observatory/observatory.factory.did.js +++ b/src/declarations/observatory/observatory.factory.did.js @@ -21,6 +21,7 @@ export const idlFactory = ({ IDL }) => { failed: IDL.Nat64 }); const OpenIdProvider = IDL.Variant({ + GitHubActions: IDL.Null, Google: IDL.Null, GitHubAuth: IDL.Null }); diff --git a/src/declarations/observatory/observatory.factory.did.mjs b/src/declarations/observatory/observatory.factory.did.mjs index 4b2ad5081f..4a5a7bbc23 100644 --- a/src/declarations/observatory/observatory.factory.did.mjs +++ b/src/declarations/observatory/observatory.factory.did.mjs @@ -21,6 +21,7 @@ export const idlFactory = ({ IDL }) => { failed: IDL.Nat64 }); const OpenIdProvider = IDL.Variant({ + GitHubActions: IDL.Null, Google: IDL.Null, GitHubAuth: IDL.Null }); diff --git a/src/declarations/satellite/satellite.did.d.ts b/src/declarations/satellite/satellite.did.d.ts index 9a9bfd5304..cb1ceab29e 100644 --- a/src/declarations/satellite/satellite.did.d.ts +++ b/src/declarations/satellite/satellite.did.d.ts @@ -34,6 +34,12 @@ export interface AssetNoContent { export interface AssetsUpgradeOptions { clear_existing_assets: [] | [boolean]; } +export type AuthenticateControllerArgs = { + OpenId: OpenIdAuthenticateControllerArgs; +}; +export type AuthenticateControllerResultResponse = + | { Ok: null } + | { Err: AuthenticationControllerError }; export type AuthenticateResultResponse = { Ok: Authentication } | { Err: AuthenticationError }; export interface Authentication { doc: Doc; @@ -56,6 +62,9 @@ export interface AuthenticationConfigOpenId { observatory_id: [] | [Principal]; providers: Array<[OpenIdDelegationProvider, OpenIdAuthProviderConfig]>; } +export type AuthenticationControllerError = + | { RegisterController: string } + | { VerifyOpenIdCredentials: VerifyOpenidAutomationCredentialsError }; export type AuthenticationError = | { PrepareDelegation: PrepareDelegationError; @@ -64,6 +73,7 @@ export type AuthenticationError = export interface AuthenticationRules { allowed_callers: Array; } +export type AutomationScope = { Write: null } | { Submit: null }; export type CollectionType = { Db: null } | { Storage: null }; export interface CommitBatch { batch_id: bigint; @@ -267,6 +277,13 @@ export interface OpenIdAuthProviderDelegationConfig { targets: [] | [Array]; max_time_to_live: [] | [bigint]; } +export interface OpenIdAuthenticateControllerArgs { + jwt: string; + metadata: Array<[string, string]>; + scope: AutomationScope; + max_time_to_live: [] | [bigint]; + controller_id: Principal; +} export type OpenIdDelegationProvider = { GitHub: null } | { Google: null }; export interface OpenIdGetDelegationArgs { jwt: string; @@ -438,8 +455,18 @@ export interface UploadChunk { export interface UploadChunkResult { chunk_id: bigint; } +export type VerifyOpenidAutomationCredentialsError = + | { + GetCachedJwks: null; + } + | { JwtVerify: JwtVerifyError } + | { GetOrFetchJwks: GetOrRefreshJwksError }; export interface _SERVICE { authenticate: ActorMethod<[AuthenticationArgs], AuthenticateResultResponse>; + authenticate_controller: ActorMethod< + [AuthenticateControllerArgs], + AuthenticateControllerResultResponse + >; commit_asset_upload: ActorMethod<[CommitBatch], undefined>; commit_proposal: ActorMethod<[CommitProposal], null>; commit_proposal_asset_upload: ActorMethod<[CommitBatch], undefined>; diff --git a/src/declarations/satellite/satellite.factory.certified.did.js b/src/declarations/satellite/satellite.factory.certified.did.js index 1a8df5df75..c44536398c 100644 --- a/src/declarations/satellite/satellite.factory.certified.did.js +++ b/src/declarations/satellite/satellite.factory.certified.did.js @@ -75,6 +75,33 @@ export const idlFactory = ({ IDL }) => { Ok: Authentication, Err: AuthenticationError }); + const AutomationScope = IDL.Variant({ + Write: IDL.Null, + Submit: IDL.Null + }); + const OpenIdAuthenticateControllerArgs = IDL.Record({ + jwt: IDL.Text, + metadata: IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text)), + scope: AutomationScope, + max_time_to_live: IDL.Opt(IDL.Nat64), + controller_id: IDL.Principal + }); + const AuthenticateControllerArgs = IDL.Variant({ + OpenId: OpenIdAuthenticateControllerArgs + }); + const VerifyOpenidAutomationCredentialsError = IDL.Variant({ + GetCachedJwks: IDL.Null, + JwtVerify: JwtVerifyError, + GetOrFetchJwks: GetOrRefreshJwksError + }); + const AuthenticationControllerError = IDL.Variant({ + RegisterController: IDL.Text, + VerifyOpenIdCredentials: VerifyOpenidAutomationCredentialsError + }); + const AuthenticateControllerResultResponse = IDL.Variant({ + Ok: IDL.Null, + Err: AuthenticationControllerError + }); const CommitBatch = IDL.Record({ batch_id: IDL.Nat, headers: IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text)), @@ -446,6 +473,11 @@ export const idlFactory = ({ IDL }) => { return IDL.Service({ authenticate: IDL.Func([AuthenticationArgs], [AuthenticateResultResponse], []), + authenticate_controller: IDL.Func( + [AuthenticateControllerArgs], + [AuthenticateControllerResultResponse], + [] + ), commit_asset_upload: IDL.Func([CommitBatch], [], []), commit_proposal: IDL.Func([CommitProposal], [IDL.Null], []), commit_proposal_asset_upload: IDL.Func([CommitBatch], [], []), diff --git a/src/declarations/satellite/satellite.factory.did.js b/src/declarations/satellite/satellite.factory.did.js index 3428decd40..697891a796 100644 --- a/src/declarations/satellite/satellite.factory.did.js +++ b/src/declarations/satellite/satellite.factory.did.js @@ -75,6 +75,33 @@ export const idlFactory = ({ IDL }) => { Ok: Authentication, Err: AuthenticationError }); + const AutomationScope = IDL.Variant({ + Write: IDL.Null, + Submit: IDL.Null + }); + const OpenIdAuthenticateControllerArgs = IDL.Record({ + jwt: IDL.Text, + metadata: IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text)), + scope: AutomationScope, + max_time_to_live: IDL.Opt(IDL.Nat64), + controller_id: IDL.Principal + }); + const AuthenticateControllerArgs = IDL.Variant({ + OpenId: OpenIdAuthenticateControllerArgs + }); + const VerifyOpenidAutomationCredentialsError = IDL.Variant({ + GetCachedJwks: IDL.Null, + JwtVerify: JwtVerifyError, + GetOrFetchJwks: GetOrRefreshJwksError + }); + const AuthenticationControllerError = IDL.Variant({ + RegisterController: IDL.Text, + VerifyOpenIdCredentials: VerifyOpenidAutomationCredentialsError + }); + const AuthenticateControllerResultResponse = IDL.Variant({ + Ok: IDL.Null, + Err: AuthenticationControllerError + }); const CommitBatch = IDL.Record({ batch_id: IDL.Nat, headers: IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text)), @@ -446,6 +473,11 @@ export const idlFactory = ({ IDL }) => { return IDL.Service({ authenticate: IDL.Func([AuthenticationArgs], [AuthenticateResultResponse], []), + authenticate_controller: IDL.Func( + [AuthenticateControllerArgs], + [AuthenticateControllerResultResponse], + [] + ), commit_asset_upload: IDL.Func([CommitBatch], [], []), commit_proposal: IDL.Func([CommitProposal], [IDL.Null], []), commit_proposal_asset_upload: IDL.Func([CommitBatch], [], []), diff --git a/src/declarations/satellite/satellite.factory.did.mjs b/src/declarations/satellite/satellite.factory.did.mjs index 3428decd40..697891a796 100644 --- a/src/declarations/satellite/satellite.factory.did.mjs +++ b/src/declarations/satellite/satellite.factory.did.mjs @@ -75,6 +75,33 @@ export const idlFactory = ({ IDL }) => { Ok: Authentication, Err: AuthenticationError }); + const AutomationScope = IDL.Variant({ + Write: IDL.Null, + Submit: IDL.Null + }); + const OpenIdAuthenticateControllerArgs = IDL.Record({ + jwt: IDL.Text, + metadata: IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text)), + scope: AutomationScope, + max_time_to_live: IDL.Opt(IDL.Nat64), + controller_id: IDL.Principal + }); + const AuthenticateControllerArgs = IDL.Variant({ + OpenId: OpenIdAuthenticateControllerArgs + }); + const VerifyOpenidAutomationCredentialsError = IDL.Variant({ + GetCachedJwks: IDL.Null, + JwtVerify: JwtVerifyError, + GetOrFetchJwks: GetOrRefreshJwksError + }); + const AuthenticationControllerError = IDL.Variant({ + RegisterController: IDL.Text, + VerifyOpenIdCredentials: VerifyOpenidAutomationCredentialsError + }); + const AuthenticateControllerResultResponse = IDL.Variant({ + Ok: IDL.Null, + Err: AuthenticationControllerError + }); const CommitBatch = IDL.Record({ batch_id: IDL.Nat, headers: IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text)), @@ -446,6 +473,11 @@ export const idlFactory = ({ IDL }) => { return IDL.Service({ authenticate: IDL.Func([AuthenticationArgs], [AuthenticateResultResponse], []), + authenticate_controller: IDL.Func( + [AuthenticateControllerArgs], + [AuthenticateControllerResultResponse], + [] + ), commit_asset_upload: IDL.Func([CommitBatch], [], []), commit_proposal: IDL.Func([CommitProposal], [IDL.Null], []), commit_proposal_asset_upload: IDL.Func([CommitBatch], [], []), diff --git a/src/declarations/sputnik/sputnik.did.d.ts b/src/declarations/sputnik/sputnik.did.d.ts index 9a9bfd5304..cb1ceab29e 100644 --- a/src/declarations/sputnik/sputnik.did.d.ts +++ b/src/declarations/sputnik/sputnik.did.d.ts @@ -34,6 +34,12 @@ export interface AssetNoContent { export interface AssetsUpgradeOptions { clear_existing_assets: [] | [boolean]; } +export type AuthenticateControllerArgs = { + OpenId: OpenIdAuthenticateControllerArgs; +}; +export type AuthenticateControllerResultResponse = + | { Ok: null } + | { Err: AuthenticationControllerError }; export type AuthenticateResultResponse = { Ok: Authentication } | { Err: AuthenticationError }; export interface Authentication { doc: Doc; @@ -56,6 +62,9 @@ export interface AuthenticationConfigOpenId { observatory_id: [] | [Principal]; providers: Array<[OpenIdDelegationProvider, OpenIdAuthProviderConfig]>; } +export type AuthenticationControllerError = + | { RegisterController: string } + | { VerifyOpenIdCredentials: VerifyOpenidAutomationCredentialsError }; export type AuthenticationError = | { PrepareDelegation: PrepareDelegationError; @@ -64,6 +73,7 @@ export type AuthenticationError = export interface AuthenticationRules { allowed_callers: Array; } +export type AutomationScope = { Write: null } | { Submit: null }; export type CollectionType = { Db: null } | { Storage: null }; export interface CommitBatch { batch_id: bigint; @@ -267,6 +277,13 @@ export interface OpenIdAuthProviderDelegationConfig { targets: [] | [Array]; max_time_to_live: [] | [bigint]; } +export interface OpenIdAuthenticateControllerArgs { + jwt: string; + metadata: Array<[string, string]>; + scope: AutomationScope; + max_time_to_live: [] | [bigint]; + controller_id: Principal; +} export type OpenIdDelegationProvider = { GitHub: null } | { Google: null }; export interface OpenIdGetDelegationArgs { jwt: string; @@ -438,8 +455,18 @@ export interface UploadChunk { export interface UploadChunkResult { chunk_id: bigint; } +export type VerifyOpenidAutomationCredentialsError = + | { + GetCachedJwks: null; + } + | { JwtVerify: JwtVerifyError } + | { GetOrFetchJwks: GetOrRefreshJwksError }; export interface _SERVICE { authenticate: ActorMethod<[AuthenticationArgs], AuthenticateResultResponse>; + authenticate_controller: ActorMethod< + [AuthenticateControllerArgs], + AuthenticateControllerResultResponse + >; commit_asset_upload: ActorMethod<[CommitBatch], undefined>; commit_proposal: ActorMethod<[CommitProposal], null>; commit_proposal_asset_upload: ActorMethod<[CommitBatch], undefined>; diff --git a/src/declarations/sputnik/sputnik.factory.certified.did.js b/src/declarations/sputnik/sputnik.factory.certified.did.js index 1a8df5df75..c44536398c 100644 --- a/src/declarations/sputnik/sputnik.factory.certified.did.js +++ b/src/declarations/sputnik/sputnik.factory.certified.did.js @@ -75,6 +75,33 @@ export const idlFactory = ({ IDL }) => { Ok: Authentication, Err: AuthenticationError }); + const AutomationScope = IDL.Variant({ + Write: IDL.Null, + Submit: IDL.Null + }); + const OpenIdAuthenticateControllerArgs = IDL.Record({ + jwt: IDL.Text, + metadata: IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text)), + scope: AutomationScope, + max_time_to_live: IDL.Opt(IDL.Nat64), + controller_id: IDL.Principal + }); + const AuthenticateControllerArgs = IDL.Variant({ + OpenId: OpenIdAuthenticateControllerArgs + }); + const VerifyOpenidAutomationCredentialsError = IDL.Variant({ + GetCachedJwks: IDL.Null, + JwtVerify: JwtVerifyError, + GetOrFetchJwks: GetOrRefreshJwksError + }); + const AuthenticationControllerError = IDL.Variant({ + RegisterController: IDL.Text, + VerifyOpenIdCredentials: VerifyOpenidAutomationCredentialsError + }); + const AuthenticateControllerResultResponse = IDL.Variant({ + Ok: IDL.Null, + Err: AuthenticationControllerError + }); const CommitBatch = IDL.Record({ batch_id: IDL.Nat, headers: IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text)), @@ -446,6 +473,11 @@ export const idlFactory = ({ IDL }) => { return IDL.Service({ authenticate: IDL.Func([AuthenticationArgs], [AuthenticateResultResponse], []), + authenticate_controller: IDL.Func( + [AuthenticateControllerArgs], + [AuthenticateControllerResultResponse], + [] + ), commit_asset_upload: IDL.Func([CommitBatch], [], []), commit_proposal: IDL.Func([CommitProposal], [IDL.Null], []), commit_proposal_asset_upload: IDL.Func([CommitBatch], [], []), diff --git a/src/declarations/sputnik/sputnik.factory.did.js b/src/declarations/sputnik/sputnik.factory.did.js index 3428decd40..697891a796 100644 --- a/src/declarations/sputnik/sputnik.factory.did.js +++ b/src/declarations/sputnik/sputnik.factory.did.js @@ -75,6 +75,33 @@ export const idlFactory = ({ IDL }) => { Ok: Authentication, Err: AuthenticationError }); + const AutomationScope = IDL.Variant({ + Write: IDL.Null, + Submit: IDL.Null + }); + const OpenIdAuthenticateControllerArgs = IDL.Record({ + jwt: IDL.Text, + metadata: IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text)), + scope: AutomationScope, + max_time_to_live: IDL.Opt(IDL.Nat64), + controller_id: IDL.Principal + }); + const AuthenticateControllerArgs = IDL.Variant({ + OpenId: OpenIdAuthenticateControllerArgs + }); + const VerifyOpenidAutomationCredentialsError = IDL.Variant({ + GetCachedJwks: IDL.Null, + JwtVerify: JwtVerifyError, + GetOrFetchJwks: GetOrRefreshJwksError + }); + const AuthenticationControllerError = IDL.Variant({ + RegisterController: IDL.Text, + VerifyOpenIdCredentials: VerifyOpenidAutomationCredentialsError + }); + const AuthenticateControllerResultResponse = IDL.Variant({ + Ok: IDL.Null, + Err: AuthenticationControllerError + }); const CommitBatch = IDL.Record({ batch_id: IDL.Nat, headers: IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text)), @@ -446,6 +473,11 @@ export const idlFactory = ({ IDL }) => { return IDL.Service({ authenticate: IDL.Func([AuthenticationArgs], [AuthenticateResultResponse], []), + authenticate_controller: IDL.Func( + [AuthenticateControllerArgs], + [AuthenticateControllerResultResponse], + [] + ), commit_asset_upload: IDL.Func([CommitBatch], [], []), commit_proposal: IDL.Func([CommitProposal], [IDL.Null], []), commit_proposal_asset_upload: IDL.Func([CommitBatch], [], []), diff --git a/src/libs/auth/src/automation/constants.rs b/src/libs/auth/src/automation/constants.rs new file mode 100644 index 0000000000..d027c00359 --- /dev/null +++ b/src/libs/auth/src/automation/constants.rs @@ -0,0 +1,7 @@ +const MINUTE_NS: u64 = 60 * 1_000_000_000; + +// 10 minutes in nanoseconds +pub const DEFAULT_EXPIRATION_PERIOD_NS: u64 = 10 * MINUTE_NS; + +// The maximum duration for a automation controller +pub const MAX_EXPIRATION_PERIOD_NS: u64 = 60 * MINUTE_NS; diff --git a/src/libs/auth/src/automation/impls.rs b/src/libs/auth/src/automation/impls.rs new file mode 100644 index 0000000000..da847697ba --- /dev/null +++ b/src/libs/auth/src/automation/impls.rs @@ -0,0 +1,17 @@ +use crate::automation::types::PrepareAutomationError; +use crate::openid::credentials::types::errors::VerifyOpenidCredentialsError; + +impl From for PrepareAutomationError { + fn from(e: VerifyOpenidCredentialsError) -> Self { + match e { + VerifyOpenidCredentialsError::GetOrFetchJwks(err) => { + PrepareAutomationError::GetOrFetchJwks(err) + } + VerifyOpenidCredentialsError::GetCachedJwks => PrepareAutomationError::GetCachedJwks, + VerifyOpenidCredentialsError::JwtFindProvider(err) => { + PrepareAutomationError::JwtFindProvider(err) + } + VerifyOpenidCredentialsError::JwtVerify(err) => PrepareAutomationError::JwtVerify(err), + } + } +} \ No newline at end of file diff --git a/src/libs/auth/src/automation/mod.rs b/src/libs/auth/src/automation/mod.rs new file mode 100644 index 0000000000..f546f4b03c --- /dev/null +++ b/src/libs/auth/src/automation/mod.rs @@ -0,0 +1,7 @@ +mod prepare; +pub mod types; +mod utils; +mod constants; +mod impls; + +pub use prepare::*; \ No newline at end of file diff --git a/src/libs/auth/src/automation/prepare.rs b/src/libs/auth/src/automation/prepare.rs new file mode 100644 index 0000000000..0390f77753 --- /dev/null +++ b/src/libs/auth/src/automation/prepare.rs @@ -0,0 +1,31 @@ +use junobuild_shared::segments::controllers::assert_controllers; +use junobuild_shared::types::state::ControllerId; +use crate::automation::types::{PrepareAutomationError, PrepareAutomationResult, PreparedAutomation, PreparedControllerAutomation}; +use crate::automation::utils::duration::build_expiration; +use crate::automation::utils::scope::build_scope; +use crate::openid::types::provider::{OpenIdAutomationProvider}; +use crate::strategies::{AuthHeapStrategy}; + +pub fn openid_prepare_automation( + controller_id: &ControllerId, + provider: &OpenIdAutomationProvider, + auth_heap: &impl AuthHeapStrategy, +) -> PrepareAutomationResult { + let controllers: [ControllerId; 1] = [controller_id.clone()]; + + assert_controllers(&controllers).map_err(PrepareAutomationError::InvalidController)?; + + // TODO: Assert do not exist + + let expires_at = build_expiration(provider, auth_heap); + + let scope = build_scope(provider, auth_heap); + + let controller: PreparedControllerAutomation = PreparedControllerAutomation { + id: controller_id.clone(), + expires_at, + scope + }; + + Ok(PreparedAutomation { controller }) +} \ No newline at end of file diff --git a/src/libs/auth/src/automation/types.rs b/src/libs/auth/src/automation/types.rs new file mode 100644 index 0000000000..f1e45026e6 --- /dev/null +++ b/src/libs/auth/src/automation/types.rs @@ -0,0 +1,43 @@ +use candid::{CandidType, Deserialize}; +use serde::Serialize; +use junobuild_shared::types::interface::SetController; +use junobuild_shared::types::state::ControllerId; +use crate::delegation::types::SessionKey; +use crate::openid::jwkset::types::errors::GetOrRefreshJwksError; +use crate::openid::jwt::types::errors::{JwtFindProviderError, JwtVerifyError}; +use crate::state::types::state::Salt; + +#[derive(CandidType, Serialize, Deserialize)] +pub struct OpenIdPrepareAutomationArgs { + pub jwt: String, + pub controller_id: ControllerId, +} + +pub type PrepareAutomationResult = Result; + +#[derive(CandidType, Serialize, Deserialize)] +pub struct PreparedAutomation { + pub controller: PreparedControllerAutomation, +} + +#[derive(CandidType, Serialize, Deserialize)] +pub struct PreparedControllerAutomation { + pub id: ControllerId, + pub scope: AutomationScope, + pub expires_at: u64, +} + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] +pub enum AutomationScope { + Write, + Submit, +} + +#[derive(CandidType, Serialize, Deserialize, Debug)] +pub enum PrepareAutomationError { + InvalidController(String), + GetOrFetchJwks(GetOrRefreshJwksError), + GetCachedJwks, + JwtFindProvider(JwtFindProviderError), + JwtVerify(JwtVerifyError), +} \ No newline at end of file diff --git a/src/libs/auth/src/automation/utils/duration.rs b/src/libs/auth/src/automation/utils/duration.rs new file mode 100644 index 0000000000..d6b3c78e4b --- /dev/null +++ b/src/libs/auth/src/automation/utils/duration.rs @@ -0,0 +1,25 @@ +use crate::automation::constants::{DEFAULT_EXPIRATION_PERIOD_NS, MAX_EXPIRATION_PERIOD_NS}; +use crate::openid::types::provider::OpenIdAutomationProvider; +use crate::state::get_automation; +use crate::strategies::AuthHeapStrategy; +use ic_cdk::api::time; +use std::cmp::min; + +pub fn build_expiration( + provider: &OpenIdAutomationProvider, + auth_heap: &impl AuthHeapStrategy, +) -> u64 { + let max_time_to_live = get_automation(auth_heap) + .as_ref() + .and_then(|automation| automation.openid.as_ref()) + .and_then(|openid| openid.providers.get(provider)) + .and_then(|openid| openid.controller.as_ref()) + .and_then(|controller| controller.max_time_to_live); + + let controller_duration = min( + max_time_to_live.unwrap_or(DEFAULT_EXPIRATION_PERIOD_NS), + MAX_EXPIRATION_PERIOD_NS, + ); + + time().saturating_add(controller_duration) +} diff --git a/src/libs/auth/src/automation/utils/mod.rs b/src/libs/auth/src/automation/utils/mod.rs new file mode 100644 index 0000000000..25a801248a --- /dev/null +++ b/src/libs/auth/src/automation/utils/mod.rs @@ -0,0 +1,2 @@ +pub mod duration; +pub mod scope; \ No newline at end of file diff --git a/src/libs/auth/src/automation/utils/scope.rs b/src/libs/auth/src/automation/utils/scope.rs new file mode 100644 index 0000000000..aa579d9040 --- /dev/null +++ b/src/libs/auth/src/automation/utils/scope.rs @@ -0,0 +1,19 @@ +use crate::automation::types::AutomationScope; +use crate::openid::types::provider::{OpenIdAutomationProvider}; +use crate::state::get_automation; +use crate::strategies::AuthHeapStrategy; + +// We default to AutomationScope::Write because practically that's what most developers use. +// i.e. most developers expect their GitHub Actions build to take effect +pub fn build_scope( + provider: &OpenIdAutomationProvider, + auth_heap: &impl AuthHeapStrategy, +) -> AutomationScope { + get_automation(auth_heap) + .as_ref() + .and_then(|automation| automation.openid.as_ref()) + .and_then(|openid| openid.providers.get(provider)) + .and_then(|openid| openid.controller.as_ref()) + .and_then(|controller| controller.scope.clone()) + .unwrap_or(AutomationScope::Write) +} \ No newline at end of file diff --git a/src/libs/auth/src/lib.rs b/src/libs/auth/src/lib.rs index 968d88c334..88bb97093e 100644 --- a/src/libs/auth/src/lib.rs +++ b/src/libs/auth/src/lib.rs @@ -4,5 +4,6 @@ pub mod profile; mod random; pub mod state; pub mod strategies; +pub mod automation; pub use state::errors; diff --git a/src/libs/auth/src/openid/credentials/automation/mod.rs b/src/libs/auth/src/openid/credentials/automation/mod.rs new file mode 100644 index 0000000000..06e9b633bf --- /dev/null +++ b/src/libs/auth/src/openid/credentials/automation/mod.rs @@ -0,0 +1,3 @@ +mod verify; + +pub use verify::*; diff --git a/src/libs/auth/src/openid/credentials/automation/verify.rs b/src/libs/auth/src/openid/credentials/automation/verify.rs new file mode 100644 index 0000000000..1ff1f9b8ee --- /dev/null +++ b/src/libs/auth/src/openid/credentials/automation/verify.rs @@ -0,0 +1,67 @@ +use crate::openid::credentials::types::errors::VerifyOpenidCredentialsError; +use crate::openid::jwkset::get_or_refresh_jwks; +use crate::openid::jwt::types::cert::Jwks; +use crate::openid::jwt::types::errors::JwtVerifyError; +use crate::openid::jwt::types::token::Claims; +use crate::openid::jwt::{unsafe_find_jwt_provider, verify_openid_jwt}; +use crate::openid::types::provider::{OpenIdAutomationProvider, OpenIdProvider}; +use crate::state::types::automation::OpenIdAutomationProviders; +use crate::strategies::AuthHeapStrategy; + +type VerifyOpenIdAutomationCredentialsResult = Result; + +pub async fn verify_openid_credentials_with_jwks_renewal( + jwt: &str, + providers: &OpenIdAutomationProviders, + auth_heap: &impl AuthHeapStrategy, +) -> VerifyOpenIdAutomationCredentialsResult { + let (automation_provider, config) = unsafe_find_jwt_provider(providers, jwt) + .map_err(VerifyOpenidCredentialsError::JwtFindProvider)?; + + let provider: OpenIdProvider = (&automation_provider).into(); + + let jwks = get_or_refresh_jwks(&provider, jwt, auth_heap) + .await + .map_err(VerifyOpenidCredentialsError::GetOrFetchJwks)?; + + verify_openid_credentials(jwt, &jwks, &automation_provider) +} + +fn verify_openid_credentials( + jwt: &str, + jwks: &Jwks, + provider: &OpenIdAutomationProvider, +) -> VerifyOpenIdAutomationCredentialsResult { + let assert_audience = |claims: &Claims| -> Result<(), JwtVerifyError> { + // if claims.aud != client_id.as_str() { + // return Err(JwtVerifyError::BadClaim("aud".to_string())); + // } + + // TODO: asser github username and repo + + Ok(()) + }; + + let assert_no_replay = |claims: &Claims| -> Result<(), JwtVerifyError> { + // let nonce = build_nonce(salt); + // + // if claims.nonce.as_deref() != Some(nonce.as_str()) { + // return Err(JwtVerifyError::BadClaim("nonce".to_string())); + // } + + // TODO: assert jti + + Ok(()) + }; + + verify_openid_jwt( + jwt, + provider.issuers(), + &jwks.keys, + &assert_audience, + &assert_no_replay, + ) + .map_err(VerifyOpenidCredentialsError::JwtVerify)?; + + Ok(provider.clone()) +} diff --git a/src/libs/auth/src/openid/credentials/delegation/verify.rs b/src/libs/auth/src/openid/credentials/delegation/verify.rs index 2501a771e9..dcf78d4616 100644 --- a/src/libs/auth/src/openid/credentials/delegation/verify.rs +++ b/src/libs/auth/src/openid/credentials/delegation/verify.rs @@ -1,7 +1,9 @@ -use crate::openid::credentials::delegation::types::interface::OpenIdDelegationCredential; use crate::openid::credentials::types::errors::VerifyOpenidCredentialsError; +use crate::openid::credentials::delegation::types::interface::OpenIdDelegationCredential; use crate::openid::jwkset::{get_jwks, get_or_refresh_jwks}; use crate::openid::jwt::types::cert::Jwks; +use crate::openid::jwt::types::errors::JwtVerifyError; +use crate::openid::jwt::types::token::Claims; use crate::openid::jwt::{unsafe_find_jwt_provider, verify_openid_jwt}; use crate::openid::types::provider::OpenIdDelegationProvider; use crate::openid::types::provider::OpenIdProvider; @@ -10,7 +12,7 @@ use crate::state::types::config::{OpenIdAuthProviderClientId, OpenIdAuthProvider use crate::state::types::state::Salt; use crate::strategies::AuthHeapStrategy; -type VerifyOpenIdCredentialsResult = +type VerifyOpenIdDelegationCredentialsResult = Result<(OpenIdDelegationCredential, OpenIdDelegationProvider), VerifyOpenidCredentialsError>; pub async fn verify_openid_credentials_with_jwks_renewal( @@ -18,7 +20,7 @@ pub async fn verify_openid_credentials_with_jwks_renewal( salt: &Salt, providers: &OpenIdAuthProviders, auth_heap: &impl AuthHeapStrategy, -) -> VerifyOpenIdCredentialsResult { +) -> VerifyOpenIdDelegationCredentialsResult { let (delegation_provider, config) = unsafe_find_jwt_provider(providers, jwt) .map_err(VerifyOpenidCredentialsError::JwtFindProvider)?; @@ -36,7 +38,7 @@ pub fn verify_openid_credentials_with_cached_jwks( salt: &Salt, providers: &OpenIdAuthProviders, auth_heap: &impl AuthHeapStrategy, -) -> VerifyOpenIdCredentialsResult { +) -> VerifyOpenIdDelegationCredentialsResult { let (delegation_provider, config) = unsafe_find_jwt_provider(providers, jwt) .map_err(VerifyOpenidCredentialsError::JwtFindProvider)?; @@ -53,11 +55,33 @@ fn verify_openid_credentials( provider: &OpenIdDelegationProvider, client_id: &OpenIdAuthProviderClientId, salt: &Salt, -) -> VerifyOpenIdCredentialsResult { - let nonce = build_nonce(salt); +) -> VerifyOpenIdDelegationCredentialsResult { + let assert_audience = |claims: &Claims| -> Result<(), JwtVerifyError> { + if claims.aud != client_id.as_str() { + return Err(JwtVerifyError::BadClaim("aud".to_string())); + } + + Ok(()) + }; + + let assert_no_replay = |claims: &Claims| -> Result<(), JwtVerifyError> { + let nonce = build_nonce(salt); + + if claims.nonce.as_deref() != Some(nonce.as_str()) { + return Err(JwtVerifyError::BadClaim("nonce".to_string())); + } + + Ok(()) + }; - let token = verify_openid_jwt(jwt, provider.issuers(), client_id, &jwks.keys, &nonce) - .map_err(VerifyOpenidCredentialsError::JwtVerify)?; + let token = verify_openid_jwt( + jwt, + provider.issuers(), + &jwks.keys, + &assert_audience, + &assert_no_replay, + ) + .map_err(VerifyOpenidCredentialsError::JwtVerify)?; let credential = OpenIdDelegationCredential::from(token); diff --git a/src/libs/auth/src/openid/credentials/mod.rs b/src/libs/auth/src/openid/credentials/mod.rs index 40a7344622..c4602919fd 100644 --- a/src/libs/auth/src/openid/credentials/mod.rs +++ b/src/libs/auth/src/openid/credentials/mod.rs @@ -1,2 +1,3 @@ pub mod delegation; +pub mod automation; pub mod types; diff --git a/src/libs/auth/src/openid/impls.rs b/src/libs/auth/src/openid/impls.rs index 99d0f42431..b76fcbdfca 100644 --- a/src/libs/auth/src/openid/impls.rs +++ b/src/libs/auth/src/openid/impls.rs @@ -1,9 +1,10 @@ use crate::openid::jwt::types::cert::Jwks; -use crate::openid::types::provider::{OpenIdCertificate, OpenIdDelegationProvider, OpenIdProvider}; +use crate::openid::types::provider::{OpenIdAutomationProvider, OpenIdCertificate, OpenIdDelegationProvider, OpenIdProvider}; use ic_cdk::api::time; use junobuild_shared::data::version::next_version; use junobuild_shared::types::state::{Version, Versioned}; use std::fmt::{Display, Formatter, Result as FmtResult}; +use crate::openid::jwt::types::provider::JwtIssuers; impl OpenIdProvider { pub fn jwks_url(&self) -> &'static str { @@ -12,6 +13,7 @@ impl OpenIdProvider { // Swap for local development with the Juno API: // http://host.docker.internal:3000/v1/auth/certs Self::GitHubAuth => "https://api.juno.build/v1/auth/certs", + Self::GitHubActions => "https://token.actions.githubusercontent.com/.well-known/jwks", } } @@ -19,6 +21,7 @@ impl OpenIdProvider { match self { OpenIdProvider::Google => &["https://accounts.google.com", "accounts.google.com"], OpenIdProvider::GitHubAuth => &["https://api.juno.build/auth/github"], + OpenIdProvider::GitHubActions => &["https://token.actions.githubusercontent.com"], } } } @@ -48,6 +51,40 @@ impl OpenIdDelegationProvider { } } +impl From<&OpenIdAutomationProvider> for OpenIdProvider { + fn from(automation_provider: &OpenIdAutomationProvider) -> Self { + match automation_provider { + OpenIdAutomationProvider::GitHub => OpenIdProvider::GitHubActions, + } + } +} + +impl OpenIdAutomationProvider { + pub fn jwks_url(&self) -> &'static str { + match self { + Self::GitHub => OpenIdProvider::GitHubActions.jwks_url(), + } + } + + pub fn issuers(&self) -> &[&'static str] { + match self { + Self::GitHub => OpenIdProvider::GitHubActions.issuers(), + } + } +} + +impl JwtIssuers for OpenIdDelegationProvider { + fn issuers(&self) -> &[&'static str] { + self.issuers() + } +} + +impl JwtIssuers for OpenIdAutomationProvider { + fn issuers(&self) -> &[&'static str] { + self.issuers() + } +} + impl Versioned for OpenIdCertificate { fn version(&self) -> Option { self.version @@ -90,7 +127,137 @@ impl Display for OpenIdProvider { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { match self { OpenIdProvider::Google => write!(f, "Google"), - OpenIdProvider::GitHubAuth => write!(f, "GitHub"), + OpenIdProvider::GitHubAuth => write!(f, "GitHub Proxy"), + OpenIdProvider::GitHubActions => write!(f, "GitHub Actions"), } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_openid_provider_jwks_urls() { + assert_eq!( + OpenIdProvider::Google.jwks_url(), + "https://www.googleapis.com/oauth2/v3/certs" + ); + assert_eq!( + OpenIdProvider::GitHubAuth.jwks_url(), + "https://api.juno.build/v1/auth/certs" + ); + assert_eq!( + OpenIdProvider::GitHubActions.jwks_url(), + "https://token.actions.githubusercontent.com/.well-known/jwks" + ); + } + + #[test] + fn test_openid_provider_issuers() { + assert_eq!( + OpenIdProvider::Google.issuers(), + &["https://accounts.google.com", "accounts.google.com"] + ); + assert_eq!( + OpenIdProvider::GitHubAuth.issuers(), + &["https://api.juno.build/auth/github"] + ); + assert_eq!( + OpenIdProvider::GitHubActions.issuers(), + &["https://token.actions.githubusercontent.com"] + ); + } + + #[test] + fn test_delegation_provider_to_openid_provider() { + assert_eq!( + OpenIdProvider::from(&OpenIdDelegationProvider::Google), + OpenIdProvider::Google + ); + assert_eq!( + OpenIdProvider::from(&OpenIdDelegationProvider::GitHub), + OpenIdProvider::GitHubAuth + ); + } + + #[test] + fn test_delegation_provider_jwks_urls() { + assert_eq!( + OpenIdDelegationProvider::Google.jwks_url(), + "https://www.googleapis.com/oauth2/v3/certs" + ); + assert_eq!( + OpenIdDelegationProvider::GitHub.jwks_url(), + "https://api.juno.build/v1/auth/certs" + ); + } + + #[test] + fn test_delegation_provider_issuers() { + assert_eq!( + OpenIdDelegationProvider::Google.issuers(), + &["https://accounts.google.com", "accounts.google.com"] + ); + assert_eq!( + OpenIdDelegationProvider::GitHub.issuers(), + &["https://api.juno.build/auth/github"] + ); + } + + #[test] + fn test_automation_provider_to_openid_provider() { + assert_eq!( + OpenIdProvider::from(&OpenIdAutomationProvider::GitHub), + OpenIdProvider::GitHubActions + ); + } + + #[test] + fn test_automation_provider_jwks_urls() { + assert_eq!( + OpenIdAutomationProvider::GitHub.jwks_url(), + "https://token.actions.githubusercontent.com/.well-known/jwks" + ); + } + + #[test] + fn test_automation_provider_issuers() { + assert_eq!( + OpenIdAutomationProvider::GitHub.issuers(), + &["https://token.actions.githubusercontent.com"] + ); + } + + #[test] + fn test_openid_certificate_init() { + let jwks = Jwks { keys: vec![] }; + let cert = OpenIdCertificate::init(&jwks); + + assert_eq!(cert.version, Some(1)); + assert_eq!(cert.created_at, cert.updated_at); + } + + #[test] + fn test_openid_certificate_update() { + let jwks = Jwks { keys: vec![] }; + let initial = OpenIdCertificate::init(&jwks); + + let new_jwks = Jwks { keys: vec![] }; + let updated = OpenIdCertificate::update(&initial, &new_jwks); + + assert_eq!(updated.version, Some(2)); + assert_eq!(updated.created_at, initial.created_at); + assert!(updated.updated_at >= initial.updated_at); + } + + #[test] + fn test_openid_provider_display() { + assert_eq!(format!("{}", OpenIdProvider::Google), "Google"); + assert_eq!(format!("{}", OpenIdProvider::GitHubAuth), "GitHub Proxy"); + assert_eq!( + format!("{}", OpenIdProvider::GitHubActions), + "GitHub Actions" + ); + } +} diff --git a/src/libs/auth/src/openid/jwt/provider.rs b/src/libs/auth/src/openid/jwt/provider.rs index 4d073898d0..060157aaef 100644 --- a/src/libs/auth/src/openid/jwt/provider.rs +++ b/src/libs/auth/src/openid/jwt/provider.rs @@ -1,16 +1,19 @@ +use std::collections::BTreeMap; use crate::openid::jwt::header::decode_jwt_header; use crate::openid::jwt::types::errors::JwtFindProviderError; use crate::openid::jwt::types::token::UnsafeClaims; -use crate::openid::types::provider::OpenIdDelegationProvider; -use crate::state::types::config::{OpenIdAuthProviderConfig, OpenIdAuthProviders}; use jsonwebtoken::dangerous; +use crate::openid::jwt::types::provider::JwtIssuers; /// ⚠️ **Warning:** This function decodes the JWT payload *without verifying its signature*. /// Use only to inspect claims (e.g., `iss`) before performing a verified decode. -pub fn unsafe_find_jwt_provider<'a>( - providers: &'a OpenIdAuthProviders, +pub fn unsafe_find_jwt_provider<'a, Provider, Config>( + providers: &'a BTreeMap, jwt: &str, -) -> Result<(OpenIdDelegationProvider, &'a OpenIdAuthProviderConfig), JwtFindProviderError> { +) -> Result<(Provider, &'a Config), JwtFindProviderError> +where + Provider: Clone + JwtIssuers, +{ // 1) Header sanity check decode_jwt_header(jwt).map_err(JwtFindProviderError::from)?; @@ -83,8 +86,8 @@ mod tests { let jwt = jwt_with(json!({"alg":"RS256"}), json!({"iss": iss})); let provs = providers_with_google(); - let (provider, _) = - unsafe_find_jwt_provider(&provs, &jwt).expect("should match even without typ"); + let (provider, _) = unsafe_find_jwt_provider(&provs, &jwt) + .expect("should match even without typ"); assert_eq!(provider, OpenIdDelegationProvider::Google); } diff --git a/src/libs/auth/src/openid/jwt/types.rs b/src/libs/auth/src/openid/jwt/types.rs index 30baa34a83..dedcc4eb23 100644 --- a/src/libs/auth/src/openid/jwt/types.rs +++ b/src/libs/auth/src/openid/jwt/types.rs @@ -193,3 +193,9 @@ pub(crate) mod errors { BadClaim(String), } } + +pub mod provider { + pub trait JwtIssuers { + fn issuers(&self) -> &[&'static str]; + } +} \ No newline at end of file diff --git a/src/libs/auth/src/openid/jwt/verify.rs b/src/libs/auth/src/openid/jwt/verify.rs index 088482f394..37d3fdc55a 100644 --- a/src/libs/auth/src/openid/jwt/verify.rs +++ b/src/libs/auth/src/openid/jwt/verify.rs @@ -7,13 +7,17 @@ fn pick_key<'a>(kid: &str, jwks: &'a [Jwk]) -> Option<&'a Jwk> { jwks.iter().find(|j| j.kid.as_deref() == Some(kid)) } -pub fn verify_openid_jwt( +pub fn verify_openid_jwt( jwt: &str, issuers: &[&str], - client_id: &str, jwks: &[Jwk], - expected_nonce: &str, -) -> Result, JwtVerifyError> { + assert_audience: Aud, + assert_no_replay: Replay, +) -> Result, JwtVerifyError> +where + Aud: FnOnce(&Claims) -> Result<(), JwtVerifyError>, + Replay: FnOnce(&Claims) -> Result<(), JwtVerifyError>, +{ // 1) Read header to get `kid` let header = decode_jwt_header(jwt).map_err(JwtVerifyError::from)?; @@ -55,16 +59,13 @@ pub fn verify_openid_jwt( let token = decode::(jwt, &key, &val).map_err(|e| JwtVerifyError::BadSig(e.to_string()))?; - // 6) Manual checks audience let c = &token.claims; - if c.aud != client_id { - return Err(JwtVerifyError::BadClaim("aud".to_string())); - } - // 7) Assert it is the expected nonce - if c.nonce.as_deref() != Some(expected_nonce) { - return Err(JwtVerifyError::BadClaim("nonce".to_string())); - } + // 6) Manual checks audience + assert_audience(c)?; + + // 7) Prevent replace attack + assert_no_replay(c)?; // 8) Assert expiration let now_ns = now_ns(); @@ -179,6 +180,20 @@ mod verify_tests { } } + fn assert_audience(claims: &Claims) -> Result<(), JwtVerifyError> { + if claims.aud != AUD_OK { + return Err(JwtVerifyError::BadClaim("aud".to_string())); + } + Ok(()) + } + + fn assert_nonce(claims: &Claims) -> Result<(), JwtVerifyError> { + if claims.nonce.as_deref() != Some(NONCE_OK) { + return Err(JwtVerifyError::BadClaim("nonce".to_string())); + } + Ok(()) + } + #[test] fn verifies_ok() { let now = now_secs(); @@ -197,9 +212,9 @@ mod verify_tests { let out = verify_openid_jwt( &token, &[ISS_GOOGLE], - AUD_OK, &[jwk_with_kid(KID_OK)], - NONCE_OK, + |claims| assert_audience(claims), + |claims| assert_nonce(claims), ) .expect("should verify"); @@ -226,9 +241,9 @@ mod verify_tests { let err = verify_openid_jwt( &token, &[ISS_GOOGLE], - AUD_OK, &[jwk_with_kid(KID_OK)], - NONCE_OK, + |claims| assert_audience(claims), + |claims| assert_nonce(claims), ) .unwrap_err(); assert!(matches!(err, JwtVerifyError::MissingKid)); @@ -252,9 +267,9 @@ mod verify_tests { let err = verify_openid_jwt( &token, &[ISS_GOOGLE], - AUD_OK, &[jwk_with_kid(KID_OK)], - NONCE_OK, + |claims| assert_audience(claims), + |claims| assert_nonce(claims), ) .unwrap_err(); assert!(matches!(err, JwtVerifyError::NoKeyForKid)); @@ -279,9 +294,9 @@ mod verify_tests { let err = verify_openid_jwt( &token, &[ISS_GOOGLE], - AUD_OK, &[jwk_with_kid(KID_OK)], - NONCE_OK, + |claims| assert_audience(claims), + |claims| assert_nonce(claims), ) .unwrap_err(); assert!(matches!(err, JwtVerifyError::BadSig(_))); @@ -305,9 +320,9 @@ mod verify_tests { let err = verify_openid_jwt( &token, &[ISS_GOOGLE], - AUD_OK, &[jwk_with_kid(KID_OK)], - NONCE_OK, + |claims| assert_audience(claims), + |claims| assert_nonce(claims), ) .unwrap_err(); assert!(matches!(err, JwtVerifyError::BadClaim(ref f) if f == "typ")); @@ -331,9 +346,9 @@ mod verify_tests { let err = verify_openid_jwt( &token, &[ISS_GOOGLE], - AUD_OK, &[jwk_with_kid(KID_OK)], - NONCE_OK, + |claims| assert_audience(claims), + |claims| assert_nonce(claims), ) .unwrap_err(); assert!(matches!(err, JwtVerifyError::BadClaim(ref f) if f == "aud")); @@ -357,9 +372,9 @@ mod verify_tests { let err = verify_openid_jwt( &token, &[ISS_GOOGLE], - AUD_OK, &[jwk_with_kid(KID_OK)], - NONCE_OK, + |claims| assert_audience(claims), + |claims| assert_nonce(claims), ) .unwrap_err(); assert!(matches!(err, JwtVerifyError::BadClaim(ref f) if f == "nonce")); @@ -384,9 +399,9 @@ mod verify_tests { let err = verify_openid_jwt( &token, &[ISS_GOOGLE], - AUD_OK, &[jwk_with_kid(KID_OK)], - NONCE_OK, + |claims| assert_audience(claims), + |claims| assert_nonce(claims), ) .unwrap_err(); assert!(matches!(err, JwtVerifyError::BadClaim(ref f) if f == "iat_future")); @@ -411,9 +426,9 @@ mod verify_tests { let err = verify_openid_jwt( &token, &[ISS_GOOGLE], - AUD_OK, &[jwk_with_kid(KID_OK)], - NONCE_OK, + |claims| assert_audience(claims), + |claims| assert_nonce(claims), ) .unwrap_err(); assert!(matches!(err, JwtVerifyError::BadClaim(ref f) if f == "iat_expired")); @@ -439,9 +454,9 @@ mod verify_tests { let err = verify_openid_jwt( &token, &[ISS_GOOGLE], - AUD_OK, &[jwk_with_kid(KID_OK)], - NONCE_OK, + |claims| assert_audience(claims), + |claims| assert_nonce(claims), ) .unwrap_err(); assert!(matches!(err, JwtVerifyError::BadSig(_))); @@ -478,8 +493,14 @@ mod verify_tests { }), }; - let err = - verify_openid_jwt(&token, &[ISS_GOOGLE], AUD_OK, &[bad_jwk], NONCE_OK).unwrap_err(); + let err = verify_openid_jwt( + &token, + &[ISS_GOOGLE], + &[bad_jwk], + |claims| assert_audience(claims), + |claims| assert_nonce(claims), + ) + .unwrap_err(); assert!(matches!(err, JwtVerifyError::BadSig(_))); } @@ -509,9 +530,9 @@ mod verify_tests { let out = verify_openid_jwt( &token, &[ISS_GOOGLE], - AUD_OK, &[jwk_with_kid(KID_OK)], - NONCE_OK, + |claims| assert_audience(claims), + |claims| assert_nonce(claims), ) .expect("should verify"); diff --git a/src/libs/auth/src/openid/types.rs b/src/libs/auth/src/openid/types.rs index e2a69bb0c9..86606e1a7a 100644 --- a/src/libs/auth/src/openid/types.rs +++ b/src/libs/auth/src/openid/types.rs @@ -10,6 +10,7 @@ pub mod provider { pub enum OpenIdProvider { Google, GitHubAuth, // GitHub user authentication (OAuth) via Juno API proxy + GitHubActions, } #[derive( @@ -20,6 +21,13 @@ pub mod provider { GitHub, } + #[derive( + CandidType, Serialize, Deserialize, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Debug, + )] + pub enum OpenIdAutomationProvider { + GitHub, + } + #[derive(CandidType, Serialize, Deserialize, Clone)] pub struct OpenIdCertificate { pub jwks: Jwks, diff --git a/src/libs/auth/src/state/errors.rs b/src/libs/auth/src/state/errors.rs index 77c596c6ae..e630223e2b 100644 --- a/src/libs/auth/src/state/errors.rs +++ b/src/libs/auth/src/state/errors.rs @@ -2,5 +2,7 @@ pub const JUNO_AUTH_ERROR_INVALID_ORIGIN: &str = "juno.auth.error.invalid_origin"; // No authentication configuration found. pub const JUNO_AUTH_ERROR_NOT_CONFIGURED: &str = "juno.auth.error.not_configured"; +// No automation configuration found. +pub const JUNO_AUTH_ERROR_AUTOMATION_NOT_CONFIGURED: &str = "juno.auth.error.automation_not_configured"; // Authentication with OpenId disabled. pub const JUNO_AUTH_ERROR_OPENID_DISABLED: &str = "juno.auth.error.openid_disabled"; diff --git a/src/libs/auth/src/state/heap.rs b/src/libs/auth/src/state/heap.rs index 18cba3e7ca..4f0e6b8443 100644 --- a/src/libs/auth/src/state/heap.rs +++ b/src/libs/auth/src/state/heap.rs @@ -4,7 +4,7 @@ use crate::state::types::state::Salt; use crate::state::types::state::{AuthenticationHeapState, OpenIdCachedCertificate, OpenIdState}; use crate::strategies::AuthHeapStrategy; use std::collections::hash_map::Entry; - +use crate::state::types::automation::AutomationConfig; // --------------------------------------------------------- // Config // --------------------------------------------------------- @@ -23,6 +23,7 @@ fn insert_config_impl(config: &AuthenticationConfig, state: &mut Option { *state = Some(AuthenticationHeapState { config: config.clone(), + automation: None, salt: None, openid: None, }) @@ -31,6 +32,35 @@ fn insert_config_impl(config: &AuthenticationConfig, state: &mut Option Option { + auth_heap + .with_auth_state(|authentication| { + authentication.as_ref().and_then(|auth| auth.automation.clone()) + }) +} + +pub fn insert_automation(auth_heap: &impl AuthHeapStrategy, automation: &Option) { + auth_heap.with_auth_state_mut(|authentication| insert_automation_impl(automation, authentication)) +} + +fn insert_automation_impl(automation: &Option, state: &mut Option) { + match state { + None => { + *state = Some(AuthenticationHeapState { + config: AuthenticationConfig::default(), + automation: automation.clone(), + salt: None, + openid: None, + }) + } + Some(state) => state.automation = automation.clone(), + } +} + // --------------------------------------------------------- // Salt // --------------------------------------------------------- @@ -48,6 +78,7 @@ fn insert_salt_impl(salt: &Salt, state: &mut Option) { None => { *state = Some(AuthenticationHeapState { config: AuthenticationConfig::default(), + automation: None, salt: Some(*salt), openid: None, }) diff --git a/src/libs/auth/src/state/mod.rs b/src/libs/auth/src/state/mod.rs index 7d5fdf1328..2989008e1c 100644 --- a/src/libs/auth/src/state/mod.rs +++ b/src/libs/auth/src/state/mod.rs @@ -10,7 +10,7 @@ pub mod types; pub use heap::{ cache_certificate, get_cached_certificate, get_config, get_openid_state, get_salt, insert_salt, - record_fetch_attempt, + record_fetch_attempt, get_automation }; pub use runtime::*; pub use store::*; diff --git a/src/libs/auth/src/state/store.rs b/src/libs/auth/src/state/store.rs index 7e727e12f9..fb990453ef 100644 --- a/src/libs/auth/src/state/store.rs +++ b/src/libs/auth/src/state/store.rs @@ -1,6 +1,6 @@ -use crate::errors::{JUNO_AUTH_ERROR_NOT_CONFIGURED, JUNO_AUTH_ERROR_OPENID_DISABLED}; +use crate::errors::{JUNO_AUTH_ERROR_AUTOMATION_NOT_CONFIGURED, JUNO_AUTH_ERROR_NOT_CONFIGURED, JUNO_AUTH_ERROR_OPENID_DISABLED}; use crate::state::assert::assert_set_config; -use crate::state::heap::get_config; +use crate::state::heap::{get_automation, get_config}; use crate::state::heap::insert_config; use crate::state::types::config::{AuthenticationConfig, OpenIdAuthProviders}; use crate::state::types::interface::SetAuthenticationConfig; @@ -8,6 +8,7 @@ use crate::state::{get_salt, insert_salt}; use crate::strategies::AuthHeapStrategy; use junobuild_shared::ic::api::print; use junobuild_shared::random::raw_rand; +use crate::state::types::automation::OpenIdAutomationProviders; pub fn set_config( auth_heap: &impl AuthHeapStrategy, @@ -56,3 +57,14 @@ pub fn get_auth_providers( Ok(openid.providers.clone()) } + +pub fn get_automation_providers( + auth_heap: &impl AuthHeapStrategy, +) -> Result { + let config = get_automation(auth_heap).ok_or(JUNO_AUTH_ERROR_AUTOMATION_NOT_CONFIGURED.to_string())?; + let openid = config + .openid + .ok_or(JUNO_AUTH_ERROR_OPENID_DISABLED.to_string())?; + + Ok(openid.providers.clone()) +} diff --git a/src/libs/auth/src/state/types.rs b/src/libs/auth/src/state/types.rs index ccae2a31c0..fa782049ae 100644 --- a/src/libs/auth/src/state/types.rs +++ b/src/libs/auth/src/state/types.rs @@ -5,12 +5,17 @@ pub mod state { use candid::CandidType; use serde::{Deserialize, Serialize}; use std::collections::HashMap; + use crate::state::types::automation::AutomationConfig; pub type Salt = [u8; 32]; #[derive(Default, CandidType, Serialize, Deserialize, Clone)] pub struct AuthenticationHeapState { + /// Configuration for user authentication via delegation (Internet Identity, Google, GitHub). + /// Note: Field name kept as "config" for backward compatibility during upgrades. pub config: AuthenticationConfig, + /// Configuration for CI/CD authentication. + pub automation: Option, pub salt: Option, pub openid: Option, } @@ -104,6 +109,62 @@ pub mod config { } } +pub mod automation { + use std::collections::{BTreeMap, HashMap}; + use candid::{CandidType, Deserialize, Principal}; + use serde::Serialize; + use junobuild_shared::types::state::{Timestamp, Version}; + use crate::automation::types::AutomationScope; + use crate::openid::types::provider::{OpenIdAutomationProvider}; + + #[derive(Default, CandidType, Serialize, Deserialize, Clone)] + pub struct AutomationConfig { + pub openid: Option, + pub version: Option, + pub created_at: Option, + pub updated_at: Option, + } + + #[derive(Default, CandidType, Serialize, Deserialize, Clone)] + pub struct AutomationConfigOpenId { + pub providers: OpenIdAutomationProviders, + pub observatory_id: Option, + } + + pub type OpenIdAutomationProviders = BTreeMap; + + // Repository identifier for GitHub automation. + // Corresponds to the `repository` claim in GitHub OIDC tokens (e.g., "octo-org/octo-repo"). + // See: https://docs.github.com/en/actions/concepts/security/openid-connect#understanding-the-oidc-token + #[derive(CandidType, Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)] + pub struct RepositoryKey { + // Repository owner (e.g. "octo-org") + pub owner: String, + // Repository name (e.g. "octo-repo") + pub name: String, + } + + pub type OpenIdAutomationRepositories = HashMap; + + #[derive(Default, CandidType, Serialize, Deserialize, Clone, Debug)] + pub struct OpenIdAutomationProviderConfig { + pub repositories: OpenIdAutomationRepositories, + pub controller: Option, + } + + #[derive(CandidType, Serialize, Deserialize, Clone, Debug)] + pub struct OpenIdAutomationRepositoryConfig { + // Optionally restrict to specific branches (e.g. ["main", "develop"]) + pub branches: Option>, + } + + #[derive(CandidType, Serialize, Deserialize, Clone, Debug)] + pub struct OpenIdAutomationProviderControllerConfig { + pub scope: Option, + pub max_time_to_live: Option, + } +} + pub mod interface { use crate::state::types::config::{ AuthenticationConfigInternetIdentity, AuthenticationConfigOpenId, AuthenticationRules, diff --git a/src/libs/satellite/satellite.did b/src/libs/satellite/satellite.did index b356cc8759..e0afc36362 100644 --- a/src/libs/satellite/satellite.did +++ b/src/libs/satellite/satellite.did @@ -20,6 +20,13 @@ type AssetNoContent = record { version : opt nat64; }; type AssetsUpgradeOptions = record { clear_existing_assets : opt bool }; +type AuthenticateControllerArgs = variant { + OpenId : OpenIdAuthenticateControllerArgs; +}; +type AuthenticateControllerResultResponse = variant { + Ok; + Err : AuthenticationControllerError; +}; type AuthenticateResultResponse = variant { Ok : Authentication; Err : AuthenticationError; @@ -42,11 +49,16 @@ type AuthenticationConfigOpenId = record { observatory_id : opt principal; providers : vec record { OpenIdDelegationProvider; OpenIdAuthProviderConfig }; }; +type AuthenticationControllerError = variant { + RegisterController : text; + VerifyOpenIdCredentials : VerifyOpenidAutomationCredentialsError; +}; type AuthenticationError = variant { PrepareDelegation : PrepareDelegationError; RegisterUser : text; }; type AuthenticationRules = record { allowed_callers : vec principal }; +type AutomationScope = variant { Write; Submit }; type CollectionType = variant { Db; Storage }; type CommitBatch = record { batch_id : nat; @@ -218,6 +230,13 @@ type OpenIdAuthProviderDelegationConfig = record { targets : opt vec principal; max_time_to_live : opt nat64; }; +type OpenIdAuthenticateControllerArgs = record { + jwt : text; + metadata : vec record { text; text }; + scope : AutomationScope; + max_time_to_live : opt nat64; + controller_id : principal; +}; type OpenIdDelegationProvider = variant { GitHub; Google }; type OpenIdGetDelegationArgs = record { jwt : text; @@ -371,8 +390,16 @@ type UploadChunk = record { order_id : opt nat; }; type UploadChunkResult = record { chunk_id : nat }; +type VerifyOpenidAutomationCredentialsError = variant { + GetCachedJwks; + JwtVerify : JwtVerifyError; + GetOrFetchJwks : GetOrRefreshJwksError; +}; service : (InitSatelliteArgs) -> { authenticate : (AuthenticationArgs) -> (AuthenticateResultResponse); + authenticate_controller : (AuthenticateControllerArgs) -> ( + AuthenticateControllerResultResponse, + ); commit_asset_upload : (CommitBatch) -> (); commit_proposal : (CommitProposal) -> (null); commit_proposal_asset_upload : (CommitBatch) -> (); diff --git a/src/libs/satellite/src/api/automation.rs b/src/libs/satellite/src/api/automation.rs new file mode 100644 index 0000000000..d46658c382 --- /dev/null +++ b/src/libs/satellite/src/api/automation.rs @@ -0,0 +1,13 @@ +use crate::automation::authenticate::openid_authenticate_automation; +use crate::automation::types::{AuthenticateAutomationArgs, AuthenticateAutomationResult}; +use junobuild_shared::ic::UnwrapOrTrap; + +pub async fn authenticate_automation( + args: AuthenticateAutomationArgs, +) -> AuthenticateAutomationResult { + match args { + AuthenticateAutomationArgs::OpenId(args) => { + openid_authenticate_automation(&args).await.unwrap_or_trap() + } + } +} diff --git a/src/libs/satellite/src/api/mod.rs b/src/libs/satellite/src/api/mod.rs index 061385cc3f..b5da645856 100644 --- a/src/libs/satellite/src/api/mod.rs +++ b/src/libs/satellite/src/api/mod.rs @@ -1,4 +1,5 @@ pub mod auth; +pub mod automation; pub mod cdn; pub mod config; pub mod controllers; diff --git a/src/libs/satellite/src/automation/authenticate.rs b/src/libs/satellite/src/automation/authenticate.rs new file mode 100644 index 0000000000..21ceab530a --- /dev/null +++ b/src/libs/satellite/src/automation/authenticate.rs @@ -0,0 +1,43 @@ +use crate::auth::strategy_impls::AuthHeap; +use crate::automation::automation; +use crate::automation::types::{AuthenticateAutomationResult, AuthenticationAutomationError}; +use crate::controllers::store::set_controllers; +use junobuild_auth::automation::types::{OpenIdPrepareAutomationArgs, PreparedAutomation}; +use junobuild_auth::state::get_automation_providers; +use junobuild_shared::types::interface::SetController; +use junobuild_shared::types::state::ControllerId; + +pub async fn openid_authenticate_automation( + args: &OpenIdPrepareAutomationArgs, + // TODO: Result> +) -> Result { + let providers = get_automation_providers(&AuthHeap)?; + + // TODO: rate? + + let prepared_automation = automation::openid_prepare_automation(args, &providers).await; + + let result = match prepared_automation { + Ok((automation, _, __)) => { + register_controller(&automation); + Ok(()) + } + Err(err) => Err(AuthenticationAutomationError::PrepareAutomation(err)), + }; + + Ok(result) +} + +fn register_controller(prepared_automation: &PreparedAutomation) { + let controllers: [ControllerId; 1] = [prepared_automation.controller.id.clone()]; + + let controller: SetController = SetController { + scope: prepared_automation.controller.scope.clone().into(), + metadata: args.metadata.clone(), + expires_at: Some(prepared_automation.controller.expires_at), + }; + + set_controllers(&controllers, &controller); + + Ok(()) +} diff --git a/src/libs/satellite/src/automation/automation.rs b/src/libs/satellite/src/automation/automation.rs new file mode 100644 index 0000000000..3426697fcf --- /dev/null +++ b/src/libs/satellite/src/automation/automation.rs @@ -0,0 +1,37 @@ +use crate::auth::strategy_impls::AuthHeap; +use junobuild_auth::automation; +use junobuild_auth::automation::types::{ + OpenIdPrepareAutomationArgs, PrepareAutomationError, PreparedAutomation, PreparedDelegation, +}; +use junobuild_auth::openid::credentials; +use junobuild_auth::openid::credentials::delegation::types::interface::OpenIdCredential; +use junobuild_auth::openid::types::provider::OpenIdDelegationProvider; +use junobuild_auth::state::types::automation::OpenIdAutomationProviders; + +pub type OpenIdPrepareAutomationResult = Result< + ( + PreparedAutomation, + OpenIdDelegationProvider, + // TODO: credential + OpenIdCredential, + ), + PrepareAutomationError, +>; + +pub async fn openid_prepare_automation( + args: &OpenIdPrepareAutomationArgs, + providers: &OpenIdAutomationProviders, +) -> OpenIdPrepareAutomationResult { + let provider = match credentials::automation::verify_openid_credentials_with_jwks_renewal( + &args.jwt, providers, &AuthHeap, + ) + .await + { + Ok(value) => value, + Err(err) => return Err(PrepareAutomationError::from(err)), + }; + + let result = automation::openid_prepare_automation(&args.controller_id, &provider, &AuthHeap); + + result.map(|prepared_delegation| (prepared_delegation, provider, credential)) +} diff --git a/src/libs/satellite/src/automation/impls.rs b/src/libs/satellite/src/automation/impls.rs new file mode 100644 index 0000000000..971d7e0769 --- /dev/null +++ b/src/libs/satellite/src/automation/impls.rs @@ -0,0 +1,11 @@ +use crate::automation::types::AutomationScope; +use junobuild_shared::types::state::ControllerScope; + +impl From for ControllerScope { + fn from(scope: AutomationScope) -> Self { + match scope { + AutomationScope::Write => ControllerScope::Write, + AutomationScope::Submit => ControllerScope::Submit, + } + } +} diff --git a/src/libs/satellite/src/automation/mod.rs b/src/libs/satellite/src/automation/mod.rs new file mode 100644 index 0000000000..7f2c56e0eb --- /dev/null +++ b/src/libs/satellite/src/automation/mod.rs @@ -0,0 +1,4 @@ +pub mod authenticate; +mod automation; +mod impls; +pub mod types; diff --git a/src/libs/satellite/src/automation/types.rs b/src/libs/satellite/src/automation/types.rs new file mode 100644 index 0000000000..00f6496828 --- /dev/null +++ b/src/libs/satellite/src/automation/types.rs @@ -0,0 +1,22 @@ +use candid::{CandidType, Deserialize}; +use junobuild_auth::automation::types::{OpenIdPrepareAutomationArgs, PrepareAutomationError}; +use serde::Serialize; + +#[derive(CandidType, Serialize, Deserialize)] +pub enum AuthenticateAutomationArgs { + OpenId(OpenIdPrepareAutomationArgs), +} + +#[derive(CandidType, Serialize, Deserialize, Clone)] +pub enum AutomationScope { + Write, + Submit, +} + +#[derive(CandidType, Serialize, Deserialize)] +pub enum AuthenticationAutomationError { + PrepareAutomation(PrepareAutomationError), + RegisterController(String), +} + +pub type AuthenticateAutomationResult = Result<(), AuthenticationAutomationError>; diff --git a/src/libs/satellite/src/impls.rs b/src/libs/satellite/src/impls.rs index 0b31f105fd..3e2abd2eff 100644 --- a/src/libs/satellite/src/impls.rs +++ b/src/libs/satellite/src/impls.rs @@ -1,6 +1,8 @@ +use crate::automation::types::AuthenticateAutomationResult; use crate::memory::internal::init_stable_state; use crate::types::interface::{ - AuthenticateResultResponse, AuthenticationResult, GetDelegationResultResponse, + AuthenticateAutomationResultResponse, AuthenticateResultResponse, AuthenticationResult, + GetDelegationResultResponse, }; use crate::types::state::{CollectionType, HeapState, RuntimeState, State}; use junobuild_auth::delegation::types::{GetDelegationError, SignedDelegation}; @@ -46,3 +48,12 @@ impl From for AuthenticateResultResponse { } } } + +impl From for AuthenticateAutomationResultResponse { + fn from(r: AuthenticateAutomationResult) -> Self { + match r { + Ok(v) => Self::Ok(v), + Err(e) => Self::Err(e), + } + } +} diff --git a/src/libs/satellite/src/lib.rs b/src/libs/satellite/src/lib.rs index 555c6c6a0b..ef2c479a64 100644 --- a/src/libs/satellite/src/lib.rs +++ b/src/libs/satellite/src/lib.rs @@ -3,6 +3,7 @@ mod api; mod assets; mod auth; +mod automation; mod certification; mod controllers; mod db; @@ -24,10 +25,11 @@ use crate::guards::{ caller_is_admin_controller, caller_is_controller, caller_is_controller_with_write, }; use crate::types::interface::{ - AuthenticateResultResponse, AuthenticationArgs, Config, DeleteProposalAssets, - GetDelegationArgs, GetDelegationResultResponse, + AuthenticateAutomationResultResponse, AuthenticateResultResponse, AuthenticationArgs, Config, + DeleteProposalAssets, GetDelegationArgs, GetDelegationResultResponse, }; use crate::types::state::CollectionType; +use automation::types::AuthenticateAutomationArgs; use ic_cdk_macros::{init, post_upgrade, pre_upgrade, query, update}; use junobuild_auth::state::types::config::AuthenticationConfig; use junobuild_auth::state::types::interface::SetAuthenticationConfig; @@ -176,6 +178,14 @@ pub fn get_delegation(args: GetDelegationArgs) -> GetDelegationResultResponse { api::auth::get_delegation(&args).into() } +#[doc(hidden)] +#[update] +pub async fn authenticate_automation( + args: AuthenticateAutomationArgs, +) -> AuthenticateAutomationResultResponse { + api::automation::authenticate_automation(args).await.into() +} + // --------------------------------------------------------- // Rules // --------------------------------------------------------- @@ -540,10 +550,10 @@ pub fn memory_size() -> MemorySize { macro_rules! include_satellite { () => { use junobuild_satellite::{ - authenticate, commit_asset_upload, commit_proposal, commit_proposal_asset_upload, - commit_proposal_many_assets_upload, count_assets, count_collection_assets, - count_collection_docs, count_docs, count_proposals, del_asset, del_assets, - del_controllers, del_custom_domain, del_doc, del_docs, del_filtered_assets, + authenticate, authenticate_automation, commit_asset_upload, commit_proposal, + commit_proposal_asset_upload, commit_proposal_many_assets_upload, count_assets, + count_collection_assets, count_collection_docs, count_docs, count_proposals, del_asset, + del_assets, del_controllers, del_custom_domain, del_doc, del_docs, del_filtered_assets, del_filtered_docs, del_many_assets, del_many_docs, del_rule, delete_proposal_assets, deposit_cycles, get_asset, get_auth_config, get_config, get_db_config, get_delegation, get_doc, get_many_assets, get_many_docs, get_proposal, get_storage_config, diff --git a/src/libs/satellite/src/types.rs b/src/libs/satellite/src/types.rs index cfcc94b79e..3e5f975faa 100644 --- a/src/libs/satellite/src/types.rs +++ b/src/libs/satellite/src/types.rs @@ -56,6 +56,7 @@ pub mod state { } pub mod interface { + use crate::automation::types::AuthenticationAutomationError; use crate::db::types::config::DbConfig; use crate::Doc; use candid::CandidType; @@ -118,6 +119,12 @@ pub mod interface { Ok(SignedDelegation), Err(GetDelegationError), } + + #[derive(CandidType, Serialize, Deserialize)] + pub enum AuthenticateAutomationResultResponse { + Ok(()), + Err(AuthenticationAutomationError), + } } pub mod store { diff --git a/src/libs/satellite/src/user/core/impls.rs b/src/libs/satellite/src/user/core/impls.rs index 2aa39684bd..193e334fdf 100644 --- a/src/libs/satellite/src/user/core/impls.rs +++ b/src/libs/satellite/src/user/core/impls.rs @@ -175,6 +175,7 @@ mod tests { use crate::user::core::types::state::{ AuthProvider, OpenIdData, ProviderData, UserData, WebAuthnData, }; + use junobuild_auth::openid::types::provider::OpenIdProvider; // ------------------------ // WebAuthnData @@ -340,12 +341,13 @@ mod tests { #[test] fn test_openid_provider_to_auth_provider() { assert!(matches!( - AuthProvider::from(&OpenIdDelegationProvider::Google), - AuthProvider::Google + AuthProvider::try_from(&OpenIdProvider::Google), + Ok(AuthProvider::Google) )); assert!(matches!( - AuthProvider::from(&OpenIdDelegationProvider::GitHub), - AuthProvider::GitHub + AuthProvider::try_from(&OpenIdProvider::GitHubAuth), + Ok(AuthProvider::GitHub) )); + assert!(AuthProvider::try_from(&OpenIdProvider::GitHubActions).is_err()); } } diff --git a/src/observatory/observatory.did b/src/observatory/observatory.did index b0a25c013f..2d134ef526 100644 --- a/src/observatory/observatory.did +++ b/src/observatory/observatory.did @@ -68,7 +68,7 @@ type OpenIdCertificate = record { created_at : nat64; version : opt nat64; }; -type OpenIdProvider = variant { Google; GitHubAuth }; +type OpenIdProvider = variant { GitHubActions; Google; GitHubAuth }; type RateConfig = record { max_tokens : nat64; time_per_token_ns : nat64 }; type RateKind = variant { OpenIdCertificateRequests }; type Segment = record { diff --git a/src/observatory/src/openid/scheduler.rs b/src/observatory/src/openid/scheduler.rs index 8cb03af9bc..06f63c206f 100644 --- a/src/observatory/src/openid/scheduler.rs +++ b/src/observatory/src/openid/scheduler.rs @@ -9,10 +9,14 @@ use std::time::Duration; pub fn defer_restart_monitoring() { // Early spare one timer if no scheduler is enabled. - let enabled_count = [OpenIdProvider::Google, OpenIdProvider::GitHubAuth] - .into_iter() - .filter(is_scheduler_enabled) - .count(); + let enabled_count = [ + OpenIdProvider::Google, + OpenIdProvider::GitHubAuth, + OpenIdProvider::GitHubActions, + ] + .into_iter() + .filter(|provider| is_scheduler_enabled(provider)) + .count(); if enabled_count == 0 { return; @@ -24,7 +28,11 @@ pub fn defer_restart_monitoring() { } async fn restart_monitoring() { - for provider in [OpenIdProvider::Google, OpenIdProvider::GitHubAuth] { + for provider in [ + OpenIdProvider::Google, + OpenIdProvider::GitHubAuth, + OpenIdProvider::GitHubActions, + ] { schedule_certificate_update(provider, None); } } diff --git a/src/satellite/satellite.did b/src/satellite/satellite.did index 52e19a508d..51e8ddd5dd 100644 --- a/src/satellite/satellite.did +++ b/src/satellite/satellite.did @@ -22,6 +22,13 @@ type AssetNoContent = record { version : opt nat64; }; type AssetsUpgradeOptions = record { clear_existing_assets : opt bool }; +type AuthenticateControllerArgs = variant { + OpenId : OpenIdAuthenticateControllerArgs; +}; +type AuthenticateControllerResultResponse = variant { + Ok; + Err : AuthenticationControllerError; +}; type AuthenticateResultResponse = variant { Ok : Authentication; Err : AuthenticationError; @@ -44,11 +51,16 @@ type AuthenticationConfigOpenId = record { observatory_id : opt principal; providers : vec record { OpenIdDelegationProvider; OpenIdAuthProviderConfig }; }; +type AuthenticationControllerError = variant { + RegisterController : text; + VerifyOpenIdCredentials : VerifyOpenidAutomationCredentialsError; +}; type AuthenticationError = variant { PrepareDelegation : PrepareDelegationError; RegisterUser : text; }; type AuthenticationRules = record { allowed_callers : vec principal }; +type AutomationScope = variant { Write; Submit }; type CollectionType = variant { Db; Storage }; type CommitBatch = record { batch_id : nat; @@ -220,6 +232,13 @@ type OpenIdAuthProviderDelegationConfig = record { targets : opt vec principal; max_time_to_live : opt nat64; }; +type OpenIdAuthenticateControllerArgs = record { + jwt : text; + metadata : vec record { text; text }; + scope : AutomationScope; + max_time_to_live : opt nat64; + controller_id : principal; +}; type OpenIdDelegationProvider = variant { GitHub; Google }; type OpenIdGetDelegationArgs = record { jwt : text; @@ -373,8 +392,16 @@ type UploadChunk = record { order_id : opt nat; }; type UploadChunkResult = record { chunk_id : nat }; +type VerifyOpenidAutomationCredentialsError = variant { + GetCachedJwks; + JwtVerify : JwtVerifyError; + GetOrFetchJwks : GetOrRefreshJwksError; +}; service : (InitSatelliteArgs) -> { authenticate : (AuthenticationArgs) -> (AuthenticateResultResponse); + authenticate_controller : (AuthenticateControllerArgs) -> ( + AuthenticateControllerResultResponse, + ); commit_asset_upload : (CommitBatch) -> (); commit_proposal : (CommitProposal) -> (null); commit_proposal_asset_upload : (CommitBatch) -> (); diff --git a/src/sputnik/sputnik.did b/src/sputnik/sputnik.did index 966966724e..22cc1bf7ad 100644 --- a/src/sputnik/sputnik.did +++ b/src/sputnik/sputnik.did @@ -22,6 +22,13 @@ type AssetNoContent = record { version : opt nat64; }; type AssetsUpgradeOptions = record { clear_existing_assets : opt bool }; +type AuthenticateControllerArgs = variant { + OpenId : OpenIdAuthenticateControllerArgs; +}; +type AuthenticateControllerResultResponse = variant { + Ok; + Err : AuthenticationControllerError; +}; type AuthenticateResultResponse = variant { Ok : Authentication; Err : AuthenticationError; @@ -44,11 +51,16 @@ type AuthenticationConfigOpenId = record { observatory_id : opt principal; providers : vec record { OpenIdDelegationProvider; OpenIdAuthProviderConfig }; }; +type AuthenticationControllerError = variant { + RegisterController : text; + VerifyOpenIdCredentials : VerifyOpenidAutomationCredentialsError; +}; type AuthenticationError = variant { PrepareDelegation : PrepareDelegationError; RegisterUser : text; }; type AuthenticationRules = record { allowed_callers : vec principal }; +type AutomationScope = variant { Write; Submit }; type CollectionType = variant { Db; Storage }; type CommitBatch = record { batch_id : nat; @@ -220,6 +232,13 @@ type OpenIdAuthProviderDelegationConfig = record { targets : opt vec principal; max_time_to_live : opt nat64; }; +type OpenIdAuthenticateControllerArgs = record { + jwt : text; + metadata : vec record { text; text }; + scope : AutomationScope; + max_time_to_live : opt nat64; + controller_id : principal; +}; type OpenIdDelegationProvider = variant { GitHub; Google }; type OpenIdGetDelegationArgs = record { jwt : text; @@ -373,8 +392,16 @@ type UploadChunk = record { order_id : opt nat; }; type UploadChunkResult = record { chunk_id : nat }; +type VerifyOpenidAutomationCredentialsError = variant { + GetCachedJwks; + JwtVerify : JwtVerifyError; + GetOrFetchJwks : GetOrRefreshJwksError; +}; service : (InitSatelliteArgs) -> { authenticate : (AuthenticationArgs) -> (AuthenticateResultResponse); + authenticate_controller : (AuthenticateControllerArgs) -> ( + AuthenticateControllerResultResponse, + ); commit_asset_upload : (CommitBatch) -> (); commit_proposal : (CommitProposal) -> (null); commit_proposal_asset_upload : (CommitBatch) -> (); diff --git a/src/tests/declarations/test_satellite/test_satellite.did.d.ts b/src/tests/declarations/test_satellite/test_satellite.did.d.ts index 6b1faaf737..c998722217 100644 --- a/src/tests/declarations/test_satellite/test_satellite.did.d.ts +++ b/src/tests/declarations/test_satellite/test_satellite.did.d.ts @@ -34,6 +34,12 @@ export interface AssetNoContent { export interface AssetsUpgradeOptions { clear_existing_assets: [] | [boolean]; } +export type AuthenticateControllerArgs = { + OpenId: OpenIdAuthenticateControllerArgs; +}; +export type AuthenticateControllerResultResponse = + | { Ok: null } + | { Err: AuthenticationControllerError }; export type AuthenticateResultResponse = { Ok: Authentication } | { Err: AuthenticationError }; export interface Authentication { doc: Doc; @@ -56,6 +62,9 @@ export interface AuthenticationConfigOpenId { observatory_id: [] | [Principal]; providers: Array<[OpenIdDelegationProvider, OpenIdAuthProviderConfig]>; } +export type AuthenticationControllerError = + | { RegisterController: string } + | { VerifyOpenIdCredentials: VerifyOpenidAutomationCredentialsError }; export type AuthenticationError = | { PrepareDelegation: PrepareDelegationError; @@ -64,6 +73,7 @@ export type AuthenticationError = export interface AuthenticationRules { allowed_callers: Array; } +export type AutomationScope = { Write: null } | { Submit: null }; export type CollectionType = { Db: null } | { Storage: null }; export interface CommitBatch { batch_id: bigint; @@ -267,6 +277,13 @@ export interface OpenIdAuthProviderDelegationConfig { targets: [] | [Array]; max_time_to_live: [] | [bigint]; } +export interface OpenIdAuthenticateControllerArgs { + jwt: string; + metadata: Array<[string, string]>; + scope: AutomationScope; + max_time_to_live: [] | [bigint]; + controller_id: Principal; +} export type OpenIdDelegationProvider = { GitHub: null } | { Google: null }; export interface OpenIdGetDelegationArgs { jwt: string; @@ -439,8 +456,18 @@ export interface UploadChunk { export interface UploadChunkResult { chunk_id: bigint; } +export type VerifyOpenidAutomationCredentialsError = + | { + GetCachedJwks: null; + } + | { JwtVerify: JwtVerifyError } + | { GetOrFetchJwks: GetOrRefreshJwksError }; export interface _SERVICE { authenticate: ActorMethod<[AuthenticationArgs], AuthenticateResultResponse>; + authenticate_controller: ActorMethod< + [AuthenticateControllerArgs], + AuthenticateControllerResultResponse + >; commit_asset_upload: ActorMethod<[CommitBatch], undefined>; commit_proposal: ActorMethod<[CommitProposal], null>; commit_proposal_asset_upload: ActorMethod<[CommitBatch], undefined>; diff --git a/src/tests/declarations/test_satellite/test_satellite.factory.certified.did.js b/src/tests/declarations/test_satellite/test_satellite.factory.certified.did.js index cf5d64c713..848902ec62 100644 --- a/src/tests/declarations/test_satellite/test_satellite.factory.certified.did.js +++ b/src/tests/declarations/test_satellite/test_satellite.factory.certified.did.js @@ -75,6 +75,33 @@ export const idlFactory = ({ IDL }) => { Ok: Authentication, Err: AuthenticationError }); + const AutomationScope = IDL.Variant({ + Write: IDL.Null, + Submit: IDL.Null + }); + const OpenIdAuthenticateControllerArgs = IDL.Record({ + jwt: IDL.Text, + metadata: IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text)), + scope: AutomationScope, + max_time_to_live: IDL.Opt(IDL.Nat64), + controller_id: IDL.Principal + }); + const AuthenticateControllerArgs = IDL.Variant({ + OpenId: OpenIdAuthenticateControllerArgs + }); + const VerifyOpenidAutomationCredentialsError = IDL.Variant({ + GetCachedJwks: IDL.Null, + JwtVerify: JwtVerifyError, + GetOrFetchJwks: GetOrRefreshJwksError + }); + const AuthenticationControllerError = IDL.Variant({ + RegisterController: IDL.Text, + VerifyOpenIdCredentials: VerifyOpenidAutomationCredentialsError + }); + const AuthenticateControllerResultResponse = IDL.Variant({ + Ok: IDL.Null, + Err: AuthenticationControllerError + }); const CommitBatch = IDL.Record({ batch_id: IDL.Nat, headers: IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text)), @@ -447,6 +474,11 @@ export const idlFactory = ({ IDL }) => { return IDL.Service({ authenticate: IDL.Func([AuthenticationArgs], [AuthenticateResultResponse], []), + authenticate_controller: IDL.Func( + [AuthenticateControllerArgs], + [AuthenticateControllerResultResponse], + [] + ), commit_asset_upload: IDL.Func([CommitBatch], [], []), commit_proposal: IDL.Func([CommitProposal], [IDL.Null], []), commit_proposal_asset_upload: IDL.Func([CommitBatch], [], []), diff --git a/src/tests/declarations/test_satellite/test_satellite.factory.did.js b/src/tests/declarations/test_satellite/test_satellite.factory.did.js index 47e8551765..673508ab0c 100644 --- a/src/tests/declarations/test_satellite/test_satellite.factory.did.js +++ b/src/tests/declarations/test_satellite/test_satellite.factory.did.js @@ -75,6 +75,33 @@ export const idlFactory = ({ IDL }) => { Ok: Authentication, Err: AuthenticationError }); + const AutomationScope = IDL.Variant({ + Write: IDL.Null, + Submit: IDL.Null + }); + const OpenIdAuthenticateControllerArgs = IDL.Record({ + jwt: IDL.Text, + metadata: IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text)), + scope: AutomationScope, + max_time_to_live: IDL.Opt(IDL.Nat64), + controller_id: IDL.Principal + }); + const AuthenticateControllerArgs = IDL.Variant({ + OpenId: OpenIdAuthenticateControllerArgs + }); + const VerifyOpenidAutomationCredentialsError = IDL.Variant({ + GetCachedJwks: IDL.Null, + JwtVerify: JwtVerifyError, + GetOrFetchJwks: GetOrRefreshJwksError + }); + const AuthenticationControllerError = IDL.Variant({ + RegisterController: IDL.Text, + VerifyOpenIdCredentials: VerifyOpenidAutomationCredentialsError + }); + const AuthenticateControllerResultResponse = IDL.Variant({ + Ok: IDL.Null, + Err: AuthenticationControllerError + }); const CommitBatch = IDL.Record({ batch_id: IDL.Nat, headers: IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text)), @@ -447,6 +474,11 @@ export const idlFactory = ({ IDL }) => { return IDL.Service({ authenticate: IDL.Func([AuthenticationArgs], [AuthenticateResultResponse], []), + authenticate_controller: IDL.Func( + [AuthenticateControllerArgs], + [AuthenticateControllerResultResponse], + [] + ), commit_asset_upload: IDL.Func([CommitBatch], [], []), commit_proposal: IDL.Func([CommitProposal], [IDL.Null], []), commit_proposal_asset_upload: IDL.Func([CommitBatch], [], []), diff --git a/src/tests/fixtures/test_satellite/test_satellite.did b/src/tests/fixtures/test_satellite/test_satellite.did index a6a3f5bb43..fa83a6fb0c 100644 --- a/src/tests/fixtures/test_satellite/test_satellite.did +++ b/src/tests/fixtures/test_satellite/test_satellite.did @@ -22,6 +22,13 @@ type AssetNoContent = record { version : opt nat64; }; type AssetsUpgradeOptions = record { clear_existing_assets : opt bool }; +type AuthenticateControllerArgs = variant { + OpenId : OpenIdAuthenticateControllerArgs; +}; +type AuthenticateControllerResultResponse = variant { + Ok; + Err : AuthenticationControllerError; +}; type AuthenticateResultResponse = variant { Ok : Authentication; Err : AuthenticationError; @@ -44,11 +51,16 @@ type AuthenticationConfigOpenId = record { observatory_id : opt principal; providers : vec record { OpenIdDelegationProvider; OpenIdAuthProviderConfig }; }; +type AuthenticationControllerError = variant { + RegisterController : text; + VerifyOpenIdCredentials : VerifyOpenidAutomationCredentialsError; +}; type AuthenticationError = variant { PrepareDelegation : PrepareDelegationError; RegisterUser : text; }; type AuthenticationRules = record { allowed_callers : vec principal }; +type AutomationScope = variant { Write; Submit }; type CollectionType = variant { Db; Storage }; type CommitBatch = record { batch_id : nat; @@ -220,6 +232,13 @@ type OpenIdAuthProviderDelegationConfig = record { targets : opt vec principal; max_time_to_live : opt nat64; }; +type OpenIdAuthenticateControllerArgs = record { + jwt : text; + metadata : vec record { text; text }; + scope : AutomationScope; + max_time_to_live : opt nat64; + controller_id : principal; +}; type OpenIdDelegationProvider = variant { GitHub; Google }; type OpenIdGetDelegationArgs = record { jwt : text; @@ -373,8 +392,16 @@ type UploadChunk = record { order_id : opt nat; }; type UploadChunkResult = record { chunk_id : nat }; +type VerifyOpenidAutomationCredentialsError = variant { + GetCachedJwks; + JwtVerify : JwtVerifyError; + GetOrFetchJwks : GetOrRefreshJwksError; +}; service : (InitSatelliteArgs) -> { authenticate : (AuthenticationArgs) -> (AuthenticateResultResponse); + authenticate_controller : (AuthenticateControllerArgs) -> ( + AuthenticateControllerResultResponse, + ); commit_asset_upload : (CommitBatch) -> (); commit_proposal : (CommitProposal) -> (null); commit_proposal_asset_upload : (CommitBatch) -> ();