diff --git a/src/declarations/satellite/satellite.did.d.ts b/src/declarations/satellite/satellite.did.d.ts index 6c29afed4a..a290a19570 100644 --- a/src/declarations/satellite/satellite.did.d.ts +++ b/src/declarations/satellite/satellite.did.d.ts @@ -34,12 +34,27 @@ export interface AssetNoContent { export interface AssetsUpgradeOptions { clear_existing_assets: [] | [boolean]; } +export type AuthenticateAutomationArgs = { + OpenId: OpenIdPrepareAutomationArgs; +}; +export type AuthenticateAutomationResultResponse = + | { + Ok: [Principal, AutomationController]; + } + | { Err: AuthenticationAutomationError }; export type AuthenticateResultResponse = { Ok: Authentication } | { Err: AuthenticationError }; export interface Authentication { doc: Doc; delegation: PreparedDelegation; } export type AuthenticationArgs = { OpenId: OpenIdPrepareDelegationArgs }; +export type AuthenticationAutomationError = + | { + PrepareAutomation: PrepareAutomationError; + } + | { RegisterController: string } + | { SaveWorkflowMetadata: string } + | { SaveUniqueJtiToken: string }; export interface AuthenticationConfig { updated_at: [] | [bigint]; openid: [] | [AuthenticationConfigOpenId]; @@ -64,6 +79,21 @@ export type AuthenticationError = export interface AuthenticationRules { allowed_callers: Array; } +export interface AutomationConfig { + updated_at: [] | [bigint]; + openid: [] | [AutomationConfigOpenId]; + created_at: [] | [bigint]; + version: [] | [bigint]; +} +export interface AutomationConfigOpenId { + observatory_id: [] | [Principal]; + providers: Array<[OpenIdAutomationProvider, OpenIdAutomationProviderConfig]>; +} +export interface AutomationController { + scope: AutomationScope; + expires_at: bigint; +} +export type AutomationScope = { Write: null } | { Submit: null }; export type CollectionType = { Db: null } | { Storage: null }; export interface CommitBatch { batch_id: bigint; @@ -78,6 +108,7 @@ export interface Config { db: [] | [DbConfig]; authentication: [] | [AuthenticationConfig]; storage: StorageConfig; + automation: [] | [AutomationConfig]; } export interface ConfigMaxMemorySize { stable: [] | [bigint]; @@ -269,6 +300,18 @@ export interface OpenIdAuthProviderDelegationConfig { targets: [] | [Array]; max_time_to_live: [] | [bigint]; } +export type OpenIdAutomationProvider = { GitHub: null }; +export interface OpenIdAutomationProviderConfig { + controller: [] | [OpenIdAutomationProviderControllerConfig]; + repositories: Array<[RepositoryKey, OpenIdAutomationRepositoryConfig]>; +} +export interface OpenIdAutomationProviderControllerConfig { + scope: [] | [AutomationScope]; + max_time_to_live: [] | [bigint]; +} +export interface OpenIdAutomationRepositoryConfig { + branches: [] | [Array]; +} export type OpenIdDelegationProvider = { GitHub: null } | { Google: null }; export interface OpenIdGetDelegationArgs { jwt: string; @@ -276,6 +319,10 @@ export interface OpenIdGetDelegationArgs { salt: Uint8Array; expiration: bigint; } +export interface OpenIdPrepareAutomationArgs { + jwt: string; + salt: Uint8Array; +} export interface OpenIdPrepareDelegationArgs { jwt: string; session_key: Uint8Array; @@ -286,6 +333,16 @@ export type Permission = | { Private: null } | { Public: null } | { Managed: null }; +export type PrepareAutomationError = + | { + JwtFindProvider: JwtFindProviderError; + } + | { InvalidController: string } + | { GetCachedJwks: null } + | { JwtVerify: JwtVerifyError } + | { GetOrFetchJwks: GetOrRefreshJwksError } + | { ControllerAlreadyExists: null } + | { TooManyControllers: string }; export type PrepareDelegationError = | { JwtFindProvider: JwtFindProviderError; @@ -325,6 +382,10 @@ export interface RateConfig { max_tokens: bigint; time_per_token_ns: bigint; } +export interface RepositoryKey { + owner: string; + name: string; +} export interface Rule { max_capacity: [] | [number]; memory: [] | [Memory]; @@ -349,6 +410,10 @@ export interface SetAuthenticationConfig { internet_identity: [] | [AuthenticationConfigInternetIdentity]; rules: [] | [AuthenticationRules]; } +export interface SetAutomationConfig { + openid: [] | [AutomationConfigOpenId]; + version: [] | [bigint]; +} export interface SetController { metadata: Array<[string, string]>; kind: [] | [ControllerKind]; @@ -443,6 +508,10 @@ export interface UploadChunkResult { } export interface _SERVICE { authenticate: ActorMethod<[AuthenticationArgs], AuthenticateResultResponse>; + authenticate_automation: ActorMethod< + [AuthenticateAutomationArgs], + AuthenticateAutomationResultResponse + >; commit_asset_upload: ActorMethod<[CommitBatch], undefined>; commit_proposal: ActorMethod<[CommitProposal], null>; commit_proposal_asset_upload: ActorMethod<[CommitBatch], undefined>; @@ -467,6 +536,7 @@ export interface _SERVICE { deposit_cycles: ActorMethod<[DepositCyclesArgs], undefined>; get_asset: ActorMethod<[string, string], [] | [AssetNoContent]>; get_auth_config: ActorMethod<[], [] | [AuthenticationConfig]>; + get_automation_config: ActorMethod<[], [] | [AutomationConfig]>; get_config: ActorMethod<[], Config>; get_db_config: ActorMethod<[], [] | [DbConfig]>; get_delegation: ActorMethod<[GetDelegationArgs], GetDelegationResultResponse>; @@ -498,6 +568,7 @@ export interface _SERVICE { reject_proposal: ActorMethod<[CommitProposal], null>; set_asset_token: ActorMethod<[string, string, [] | [string]], undefined>; set_auth_config: ActorMethod<[SetAuthenticationConfig], AuthenticationConfig>; + set_automation_config: ActorMethod<[SetAutomationConfig], AutomationConfig>; set_controllers: ActorMethod<[SetControllersArgs], Array<[Principal, Controller]>>; set_custom_domain: ActorMethod<[string, [] | [string]], undefined>; set_db_config: ActorMethod<[SetDbConfig], DbConfig>; diff --git a/src/declarations/satellite/satellite.factory.certified.did.js b/src/declarations/satellite/satellite.factory.certified.did.js index 3926f90a01..db425c6b99 100644 --- a/src/declarations/satellite/satellite.factory.certified.did.js +++ b/src/declarations/satellite/satellite.factory.certified.did.js @@ -75,6 +75,40 @@ export const idlFactory = ({ IDL }) => { Ok: Authentication, Err: AuthenticationError }); + const OpenIdPrepareAutomationArgs = IDL.Record({ + jwt: IDL.Text, + salt: IDL.Vec(IDL.Nat8) + }); + const AuthenticateAutomationArgs = IDL.Variant({ + OpenId: OpenIdPrepareAutomationArgs + }); + const AutomationScope = IDL.Variant({ + Write: IDL.Null, + Submit: IDL.Null + }); + const AutomationController = IDL.Record({ + scope: AutomationScope, + expires_at: IDL.Nat64 + }); + const PrepareAutomationError = IDL.Variant({ + JwtFindProvider: JwtFindProviderError, + InvalidController: IDL.Text, + GetCachedJwks: IDL.Null, + JwtVerify: JwtVerifyError, + GetOrFetchJwks: GetOrRefreshJwksError, + ControllerAlreadyExists: IDL.Null, + TooManyControllers: IDL.Text + }); + const AuthenticationAutomationError = IDL.Variant({ + PrepareAutomation: PrepareAutomationError, + RegisterController: IDL.Text, + SaveWorkflowMetadata: IDL.Text, + SaveUniqueJtiToken: IDL.Text + }); + const AuthenticateAutomationResultResponse = IDL.Variant({ + Ok: IDL.Tuple(IDL.Principal, AutomationController), + Err: AuthenticationAutomationError + }); const CommitBatch = IDL.Record({ batch_id: IDL.Nat, headers: IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text)), @@ -194,6 +228,29 @@ export const idlFactory = ({ IDL }) => { internet_identity: IDL.Opt(AuthenticationConfigInternetIdentity), rules: IDL.Opt(AuthenticationRules) }); + const OpenIdAutomationProvider = IDL.Variant({ GitHub: IDL.Null }); + const OpenIdAutomationProviderControllerConfig = IDL.Record({ + scope: IDL.Opt(AutomationScope), + max_time_to_live: IDL.Opt(IDL.Nat64) + }); + const RepositoryKey = IDL.Record({ owner: IDL.Text, name: IDL.Text }); + const OpenIdAutomationRepositoryConfig = IDL.Record({ + branches: IDL.Opt(IDL.Vec(IDL.Text)) + }); + const OpenIdAutomationProviderConfig = IDL.Record({ + controller: IDL.Opt(OpenIdAutomationProviderControllerConfig), + repositories: IDL.Vec(IDL.Tuple(RepositoryKey, OpenIdAutomationRepositoryConfig)) + }); + const AutomationConfigOpenId = IDL.Record({ + observatory_id: IDL.Opt(IDL.Principal), + providers: IDL.Vec(IDL.Tuple(OpenIdAutomationProvider, OpenIdAutomationProviderConfig)) + }); + const AutomationConfig = IDL.Record({ + updated_at: IDL.Opt(IDL.Nat64), + openid: IDL.Opt(AutomationConfigOpenId), + created_at: IDL.Opt(IDL.Nat64), + version: IDL.Opt(IDL.Nat64) + }); const ConfigMaxMemorySize = IDL.Record({ stable: IDL.Opt(IDL.Nat64), heap: IDL.Opt(IDL.Nat64) @@ -231,7 +288,8 @@ export const idlFactory = ({ IDL }) => { const Config = IDL.Record({ db: IDL.Opt(DbConfig), authentication: IDL.Opt(AuthenticationConfig), - storage: StorageConfig + storage: StorageConfig, + automation: IDL.Opt(AutomationConfig) }); const OpenIdGetDelegationArgs = IDL.Record({ jwt: IDL.Text, @@ -404,6 +462,10 @@ export const idlFactory = ({ IDL }) => { internet_identity: IDL.Opt(AuthenticationConfigInternetIdentity), rules: IDL.Opt(AuthenticationRules) }); + const SetAutomationConfig = IDL.Record({ + openid: IDL.Opt(AutomationConfigOpenId), + version: IDL.Opt(IDL.Nat64) + }); const SetController = IDL.Record({ metadata: IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text)), kind: IDL.Opt(ControllerKind), @@ -452,6 +514,11 @@ export const idlFactory = ({ IDL }) => { return IDL.Service({ authenticate: IDL.Func([AuthenticationArgs], [AuthenticateResultResponse], []), + authenticate_automation: IDL.Func( + [AuthenticateAutomationArgs], + [AuthenticateAutomationResultResponse], + [] + ), commit_asset_upload: IDL.Func([CommitBatch], [], []), commit_proposal: IDL.Func([CommitProposal], [IDL.Null], []), commit_proposal_asset_upload: IDL.Func([CommitBatch], [], []), @@ -480,6 +547,7 @@ export const idlFactory = ({ IDL }) => { deposit_cycles: IDL.Func([DepositCyclesArgs], [], []), get_asset: IDL.Func([IDL.Text, IDL.Text], [IDL.Opt(AssetNoContent)], []), get_auth_config: IDL.Func([], [IDL.Opt(AuthenticationConfig)], []), + get_automation_config: IDL.Func([], [IDL.Opt(AutomationConfig)], []), get_config: IDL.Func([], [Config], []), get_db_config: IDL.Func([], [IDL.Opt(DbConfig)], []), get_delegation: IDL.Func([GetDelegationArgs], [GetDelegationResultResponse], []), @@ -521,6 +589,7 @@ export const idlFactory = ({ IDL }) => { reject_proposal: IDL.Func([CommitProposal], [IDL.Null], []), set_asset_token: IDL.Func([IDL.Text, IDL.Text, IDL.Opt(IDL.Text)], [], []), set_auth_config: IDL.Func([SetAuthenticationConfig], [AuthenticationConfig], []), + set_automation_config: IDL.Func([SetAutomationConfig], [AutomationConfig], []), set_controllers: IDL.Func( [SetControllersArgs], [IDL.Vec(IDL.Tuple(IDL.Principal, Controller))], diff --git a/src/declarations/satellite/satellite.factory.did.js b/src/declarations/satellite/satellite.factory.did.js index 74a3f16d91..ea3bbd4157 100644 --- a/src/declarations/satellite/satellite.factory.did.js +++ b/src/declarations/satellite/satellite.factory.did.js @@ -75,6 +75,40 @@ export const idlFactory = ({ IDL }) => { Ok: Authentication, Err: AuthenticationError }); + const OpenIdPrepareAutomationArgs = IDL.Record({ + jwt: IDL.Text, + salt: IDL.Vec(IDL.Nat8) + }); + const AuthenticateAutomationArgs = IDL.Variant({ + OpenId: OpenIdPrepareAutomationArgs + }); + const AutomationScope = IDL.Variant({ + Write: IDL.Null, + Submit: IDL.Null + }); + const AutomationController = IDL.Record({ + scope: AutomationScope, + expires_at: IDL.Nat64 + }); + const PrepareAutomationError = IDL.Variant({ + JwtFindProvider: JwtFindProviderError, + InvalidController: IDL.Text, + GetCachedJwks: IDL.Null, + JwtVerify: JwtVerifyError, + GetOrFetchJwks: GetOrRefreshJwksError, + ControllerAlreadyExists: IDL.Null, + TooManyControllers: IDL.Text + }); + const AuthenticationAutomationError = IDL.Variant({ + PrepareAutomation: PrepareAutomationError, + RegisterController: IDL.Text, + SaveWorkflowMetadata: IDL.Text, + SaveUniqueJtiToken: IDL.Text + }); + const AuthenticateAutomationResultResponse = IDL.Variant({ + Ok: IDL.Tuple(IDL.Principal, AutomationController), + Err: AuthenticationAutomationError + }); const CommitBatch = IDL.Record({ batch_id: IDL.Nat, headers: IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text)), @@ -194,6 +228,29 @@ export const idlFactory = ({ IDL }) => { internet_identity: IDL.Opt(AuthenticationConfigInternetIdentity), rules: IDL.Opt(AuthenticationRules) }); + const OpenIdAutomationProvider = IDL.Variant({ GitHub: IDL.Null }); + const OpenIdAutomationProviderControllerConfig = IDL.Record({ + scope: IDL.Opt(AutomationScope), + max_time_to_live: IDL.Opt(IDL.Nat64) + }); + const RepositoryKey = IDL.Record({ owner: IDL.Text, name: IDL.Text }); + const OpenIdAutomationRepositoryConfig = IDL.Record({ + branches: IDL.Opt(IDL.Vec(IDL.Text)) + }); + const OpenIdAutomationProviderConfig = IDL.Record({ + controller: IDL.Opt(OpenIdAutomationProviderControllerConfig), + repositories: IDL.Vec(IDL.Tuple(RepositoryKey, OpenIdAutomationRepositoryConfig)) + }); + const AutomationConfigOpenId = IDL.Record({ + observatory_id: IDL.Opt(IDL.Principal), + providers: IDL.Vec(IDL.Tuple(OpenIdAutomationProvider, OpenIdAutomationProviderConfig)) + }); + const AutomationConfig = IDL.Record({ + updated_at: IDL.Opt(IDL.Nat64), + openid: IDL.Opt(AutomationConfigOpenId), + created_at: IDL.Opt(IDL.Nat64), + version: IDL.Opt(IDL.Nat64) + }); const ConfigMaxMemorySize = IDL.Record({ stable: IDL.Opt(IDL.Nat64), heap: IDL.Opt(IDL.Nat64) @@ -231,7 +288,8 @@ export const idlFactory = ({ IDL }) => { const Config = IDL.Record({ db: IDL.Opt(DbConfig), authentication: IDL.Opt(AuthenticationConfig), - storage: StorageConfig + storage: StorageConfig, + automation: IDL.Opt(AutomationConfig) }); const OpenIdGetDelegationArgs = IDL.Record({ jwt: IDL.Text, @@ -404,6 +462,10 @@ export const idlFactory = ({ IDL }) => { internet_identity: IDL.Opt(AuthenticationConfigInternetIdentity), rules: IDL.Opt(AuthenticationRules) }); + const SetAutomationConfig = IDL.Record({ + openid: IDL.Opt(AutomationConfigOpenId), + version: IDL.Opt(IDL.Nat64) + }); const SetController = IDL.Record({ metadata: IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text)), kind: IDL.Opt(ControllerKind), @@ -452,6 +514,11 @@ export const idlFactory = ({ IDL }) => { return IDL.Service({ authenticate: IDL.Func([AuthenticationArgs], [AuthenticateResultResponse], []), + authenticate_automation: IDL.Func( + [AuthenticateAutomationArgs], + [AuthenticateAutomationResultResponse], + [] + ), commit_asset_upload: IDL.Func([CommitBatch], [], []), commit_proposal: IDL.Func([CommitProposal], [IDL.Null], []), commit_proposal_asset_upload: IDL.Func([CommitBatch], [], []), @@ -480,6 +547,7 @@ export const idlFactory = ({ IDL }) => { deposit_cycles: IDL.Func([DepositCyclesArgs], [], []), get_asset: IDL.Func([IDL.Text, IDL.Text], [IDL.Opt(AssetNoContent)], ['query']), get_auth_config: IDL.Func([], [IDL.Opt(AuthenticationConfig)], ['query']), + get_automation_config: IDL.Func([], [IDL.Opt(AutomationConfig)], ['query']), get_config: IDL.Func([], [Config], []), get_db_config: IDL.Func([], [IDL.Opt(DbConfig)], ['query']), get_delegation: IDL.Func([GetDelegationArgs], [GetDelegationResultResponse], ['query']), @@ -521,6 +589,7 @@ export const idlFactory = ({ IDL }) => { reject_proposal: IDL.Func([CommitProposal], [IDL.Null], []), set_asset_token: IDL.Func([IDL.Text, IDL.Text, IDL.Opt(IDL.Text)], [], []), set_auth_config: IDL.Func([SetAuthenticationConfig], [AuthenticationConfig], []), + set_automation_config: IDL.Func([SetAutomationConfig], [AutomationConfig], []), set_controllers: IDL.Func( [SetControllersArgs], [IDL.Vec(IDL.Tuple(IDL.Principal, Controller))], diff --git a/src/declarations/satellite/satellite.factory.did.mjs b/src/declarations/satellite/satellite.factory.did.mjs index 74a3f16d91..ea3bbd4157 100644 --- a/src/declarations/satellite/satellite.factory.did.mjs +++ b/src/declarations/satellite/satellite.factory.did.mjs @@ -75,6 +75,40 @@ export const idlFactory = ({ IDL }) => { Ok: Authentication, Err: AuthenticationError }); + const OpenIdPrepareAutomationArgs = IDL.Record({ + jwt: IDL.Text, + salt: IDL.Vec(IDL.Nat8) + }); + const AuthenticateAutomationArgs = IDL.Variant({ + OpenId: OpenIdPrepareAutomationArgs + }); + const AutomationScope = IDL.Variant({ + Write: IDL.Null, + Submit: IDL.Null + }); + const AutomationController = IDL.Record({ + scope: AutomationScope, + expires_at: IDL.Nat64 + }); + const PrepareAutomationError = IDL.Variant({ + JwtFindProvider: JwtFindProviderError, + InvalidController: IDL.Text, + GetCachedJwks: IDL.Null, + JwtVerify: JwtVerifyError, + GetOrFetchJwks: GetOrRefreshJwksError, + ControllerAlreadyExists: IDL.Null, + TooManyControllers: IDL.Text + }); + const AuthenticationAutomationError = IDL.Variant({ + PrepareAutomation: PrepareAutomationError, + RegisterController: IDL.Text, + SaveWorkflowMetadata: IDL.Text, + SaveUniqueJtiToken: IDL.Text + }); + const AuthenticateAutomationResultResponse = IDL.Variant({ + Ok: IDL.Tuple(IDL.Principal, AutomationController), + Err: AuthenticationAutomationError + }); const CommitBatch = IDL.Record({ batch_id: IDL.Nat, headers: IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text)), @@ -194,6 +228,29 @@ export const idlFactory = ({ IDL }) => { internet_identity: IDL.Opt(AuthenticationConfigInternetIdentity), rules: IDL.Opt(AuthenticationRules) }); + const OpenIdAutomationProvider = IDL.Variant({ GitHub: IDL.Null }); + const OpenIdAutomationProviderControllerConfig = IDL.Record({ + scope: IDL.Opt(AutomationScope), + max_time_to_live: IDL.Opt(IDL.Nat64) + }); + const RepositoryKey = IDL.Record({ owner: IDL.Text, name: IDL.Text }); + const OpenIdAutomationRepositoryConfig = IDL.Record({ + branches: IDL.Opt(IDL.Vec(IDL.Text)) + }); + const OpenIdAutomationProviderConfig = IDL.Record({ + controller: IDL.Opt(OpenIdAutomationProviderControllerConfig), + repositories: IDL.Vec(IDL.Tuple(RepositoryKey, OpenIdAutomationRepositoryConfig)) + }); + const AutomationConfigOpenId = IDL.Record({ + observatory_id: IDL.Opt(IDL.Principal), + providers: IDL.Vec(IDL.Tuple(OpenIdAutomationProvider, OpenIdAutomationProviderConfig)) + }); + const AutomationConfig = IDL.Record({ + updated_at: IDL.Opt(IDL.Nat64), + openid: IDL.Opt(AutomationConfigOpenId), + created_at: IDL.Opt(IDL.Nat64), + version: IDL.Opt(IDL.Nat64) + }); const ConfigMaxMemorySize = IDL.Record({ stable: IDL.Opt(IDL.Nat64), heap: IDL.Opt(IDL.Nat64) @@ -231,7 +288,8 @@ export const idlFactory = ({ IDL }) => { const Config = IDL.Record({ db: IDL.Opt(DbConfig), authentication: IDL.Opt(AuthenticationConfig), - storage: StorageConfig + storage: StorageConfig, + automation: IDL.Opt(AutomationConfig) }); const OpenIdGetDelegationArgs = IDL.Record({ jwt: IDL.Text, @@ -404,6 +462,10 @@ export const idlFactory = ({ IDL }) => { internet_identity: IDL.Opt(AuthenticationConfigInternetIdentity), rules: IDL.Opt(AuthenticationRules) }); + const SetAutomationConfig = IDL.Record({ + openid: IDL.Opt(AutomationConfigOpenId), + version: IDL.Opt(IDL.Nat64) + }); const SetController = IDL.Record({ metadata: IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text)), kind: IDL.Opt(ControllerKind), @@ -452,6 +514,11 @@ export const idlFactory = ({ IDL }) => { return IDL.Service({ authenticate: IDL.Func([AuthenticationArgs], [AuthenticateResultResponse], []), + authenticate_automation: IDL.Func( + [AuthenticateAutomationArgs], + [AuthenticateAutomationResultResponse], + [] + ), commit_asset_upload: IDL.Func([CommitBatch], [], []), commit_proposal: IDL.Func([CommitProposal], [IDL.Null], []), commit_proposal_asset_upload: IDL.Func([CommitBatch], [], []), @@ -480,6 +547,7 @@ export const idlFactory = ({ IDL }) => { deposit_cycles: IDL.Func([DepositCyclesArgs], [], []), get_asset: IDL.Func([IDL.Text, IDL.Text], [IDL.Opt(AssetNoContent)], ['query']), get_auth_config: IDL.Func([], [IDL.Opt(AuthenticationConfig)], ['query']), + get_automation_config: IDL.Func([], [IDL.Opt(AutomationConfig)], ['query']), get_config: IDL.Func([], [Config], []), get_db_config: IDL.Func([], [IDL.Opt(DbConfig)], ['query']), get_delegation: IDL.Func([GetDelegationArgs], [GetDelegationResultResponse], ['query']), @@ -521,6 +589,7 @@ export const idlFactory = ({ IDL }) => { reject_proposal: IDL.Func([CommitProposal], [IDL.Null], []), set_asset_token: IDL.Func([IDL.Text, IDL.Text, IDL.Opt(IDL.Text)], [], []), set_auth_config: IDL.Func([SetAuthenticationConfig], [AuthenticationConfig], []), + set_automation_config: IDL.Func([SetAutomationConfig], [AutomationConfig], []), set_controllers: IDL.Func( [SetControllersArgs], [IDL.Vec(IDL.Tuple(IDL.Principal, Controller))], diff --git a/src/declarations/sputnik/sputnik.did.d.ts b/src/declarations/sputnik/sputnik.did.d.ts index 6c29afed4a..a290a19570 100644 --- a/src/declarations/sputnik/sputnik.did.d.ts +++ b/src/declarations/sputnik/sputnik.did.d.ts @@ -34,12 +34,27 @@ export interface AssetNoContent { export interface AssetsUpgradeOptions { clear_existing_assets: [] | [boolean]; } +export type AuthenticateAutomationArgs = { + OpenId: OpenIdPrepareAutomationArgs; +}; +export type AuthenticateAutomationResultResponse = + | { + Ok: [Principal, AutomationController]; + } + | { Err: AuthenticationAutomationError }; export type AuthenticateResultResponse = { Ok: Authentication } | { Err: AuthenticationError }; export interface Authentication { doc: Doc; delegation: PreparedDelegation; } export type AuthenticationArgs = { OpenId: OpenIdPrepareDelegationArgs }; +export type AuthenticationAutomationError = + | { + PrepareAutomation: PrepareAutomationError; + } + | { RegisterController: string } + | { SaveWorkflowMetadata: string } + | { SaveUniqueJtiToken: string }; export interface AuthenticationConfig { updated_at: [] | [bigint]; openid: [] | [AuthenticationConfigOpenId]; @@ -64,6 +79,21 @@ export type AuthenticationError = export interface AuthenticationRules { allowed_callers: Array; } +export interface AutomationConfig { + updated_at: [] | [bigint]; + openid: [] | [AutomationConfigOpenId]; + created_at: [] | [bigint]; + version: [] | [bigint]; +} +export interface AutomationConfigOpenId { + observatory_id: [] | [Principal]; + providers: Array<[OpenIdAutomationProvider, OpenIdAutomationProviderConfig]>; +} +export interface AutomationController { + scope: AutomationScope; + expires_at: bigint; +} +export type AutomationScope = { Write: null } | { Submit: null }; export type CollectionType = { Db: null } | { Storage: null }; export interface CommitBatch { batch_id: bigint; @@ -78,6 +108,7 @@ export interface Config { db: [] | [DbConfig]; authentication: [] | [AuthenticationConfig]; storage: StorageConfig; + automation: [] | [AutomationConfig]; } export interface ConfigMaxMemorySize { stable: [] | [bigint]; @@ -269,6 +300,18 @@ export interface OpenIdAuthProviderDelegationConfig { targets: [] | [Array]; max_time_to_live: [] | [bigint]; } +export type OpenIdAutomationProvider = { GitHub: null }; +export interface OpenIdAutomationProviderConfig { + controller: [] | [OpenIdAutomationProviderControllerConfig]; + repositories: Array<[RepositoryKey, OpenIdAutomationRepositoryConfig]>; +} +export interface OpenIdAutomationProviderControllerConfig { + scope: [] | [AutomationScope]; + max_time_to_live: [] | [bigint]; +} +export interface OpenIdAutomationRepositoryConfig { + branches: [] | [Array]; +} export type OpenIdDelegationProvider = { GitHub: null } | { Google: null }; export interface OpenIdGetDelegationArgs { jwt: string; @@ -276,6 +319,10 @@ export interface OpenIdGetDelegationArgs { salt: Uint8Array; expiration: bigint; } +export interface OpenIdPrepareAutomationArgs { + jwt: string; + salt: Uint8Array; +} export interface OpenIdPrepareDelegationArgs { jwt: string; session_key: Uint8Array; @@ -286,6 +333,16 @@ export type Permission = | { Private: null } | { Public: null } | { Managed: null }; +export type PrepareAutomationError = + | { + JwtFindProvider: JwtFindProviderError; + } + | { InvalidController: string } + | { GetCachedJwks: null } + | { JwtVerify: JwtVerifyError } + | { GetOrFetchJwks: GetOrRefreshJwksError } + | { ControllerAlreadyExists: null } + | { TooManyControllers: string }; export type PrepareDelegationError = | { JwtFindProvider: JwtFindProviderError; @@ -325,6 +382,10 @@ export interface RateConfig { max_tokens: bigint; time_per_token_ns: bigint; } +export interface RepositoryKey { + owner: string; + name: string; +} export interface Rule { max_capacity: [] | [number]; memory: [] | [Memory]; @@ -349,6 +410,10 @@ export interface SetAuthenticationConfig { internet_identity: [] | [AuthenticationConfigInternetIdentity]; rules: [] | [AuthenticationRules]; } +export interface SetAutomationConfig { + openid: [] | [AutomationConfigOpenId]; + version: [] | [bigint]; +} export interface SetController { metadata: Array<[string, string]>; kind: [] | [ControllerKind]; @@ -443,6 +508,10 @@ export interface UploadChunkResult { } export interface _SERVICE { authenticate: ActorMethod<[AuthenticationArgs], AuthenticateResultResponse>; + authenticate_automation: ActorMethod< + [AuthenticateAutomationArgs], + AuthenticateAutomationResultResponse + >; commit_asset_upload: ActorMethod<[CommitBatch], undefined>; commit_proposal: ActorMethod<[CommitProposal], null>; commit_proposal_asset_upload: ActorMethod<[CommitBatch], undefined>; @@ -467,6 +536,7 @@ export interface _SERVICE { deposit_cycles: ActorMethod<[DepositCyclesArgs], undefined>; get_asset: ActorMethod<[string, string], [] | [AssetNoContent]>; get_auth_config: ActorMethod<[], [] | [AuthenticationConfig]>; + get_automation_config: ActorMethod<[], [] | [AutomationConfig]>; get_config: ActorMethod<[], Config>; get_db_config: ActorMethod<[], [] | [DbConfig]>; get_delegation: ActorMethod<[GetDelegationArgs], GetDelegationResultResponse>; @@ -498,6 +568,7 @@ export interface _SERVICE { reject_proposal: ActorMethod<[CommitProposal], null>; set_asset_token: ActorMethod<[string, string, [] | [string]], undefined>; set_auth_config: ActorMethod<[SetAuthenticationConfig], AuthenticationConfig>; + set_automation_config: ActorMethod<[SetAutomationConfig], AutomationConfig>; set_controllers: ActorMethod<[SetControllersArgs], Array<[Principal, Controller]>>; set_custom_domain: ActorMethod<[string, [] | [string]], undefined>; set_db_config: ActorMethod<[SetDbConfig], DbConfig>; diff --git a/src/declarations/sputnik/sputnik.factory.certified.did.js b/src/declarations/sputnik/sputnik.factory.certified.did.js index 3926f90a01..db425c6b99 100644 --- a/src/declarations/sputnik/sputnik.factory.certified.did.js +++ b/src/declarations/sputnik/sputnik.factory.certified.did.js @@ -75,6 +75,40 @@ export const idlFactory = ({ IDL }) => { Ok: Authentication, Err: AuthenticationError }); + const OpenIdPrepareAutomationArgs = IDL.Record({ + jwt: IDL.Text, + salt: IDL.Vec(IDL.Nat8) + }); + const AuthenticateAutomationArgs = IDL.Variant({ + OpenId: OpenIdPrepareAutomationArgs + }); + const AutomationScope = IDL.Variant({ + Write: IDL.Null, + Submit: IDL.Null + }); + const AutomationController = IDL.Record({ + scope: AutomationScope, + expires_at: IDL.Nat64 + }); + const PrepareAutomationError = IDL.Variant({ + JwtFindProvider: JwtFindProviderError, + InvalidController: IDL.Text, + GetCachedJwks: IDL.Null, + JwtVerify: JwtVerifyError, + GetOrFetchJwks: GetOrRefreshJwksError, + ControllerAlreadyExists: IDL.Null, + TooManyControllers: IDL.Text + }); + const AuthenticationAutomationError = IDL.Variant({ + PrepareAutomation: PrepareAutomationError, + RegisterController: IDL.Text, + SaveWorkflowMetadata: IDL.Text, + SaveUniqueJtiToken: IDL.Text + }); + const AuthenticateAutomationResultResponse = IDL.Variant({ + Ok: IDL.Tuple(IDL.Principal, AutomationController), + Err: AuthenticationAutomationError + }); const CommitBatch = IDL.Record({ batch_id: IDL.Nat, headers: IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text)), @@ -194,6 +228,29 @@ export const idlFactory = ({ IDL }) => { internet_identity: IDL.Opt(AuthenticationConfigInternetIdentity), rules: IDL.Opt(AuthenticationRules) }); + const OpenIdAutomationProvider = IDL.Variant({ GitHub: IDL.Null }); + const OpenIdAutomationProviderControllerConfig = IDL.Record({ + scope: IDL.Opt(AutomationScope), + max_time_to_live: IDL.Opt(IDL.Nat64) + }); + const RepositoryKey = IDL.Record({ owner: IDL.Text, name: IDL.Text }); + const OpenIdAutomationRepositoryConfig = IDL.Record({ + branches: IDL.Opt(IDL.Vec(IDL.Text)) + }); + const OpenIdAutomationProviderConfig = IDL.Record({ + controller: IDL.Opt(OpenIdAutomationProviderControllerConfig), + repositories: IDL.Vec(IDL.Tuple(RepositoryKey, OpenIdAutomationRepositoryConfig)) + }); + const AutomationConfigOpenId = IDL.Record({ + observatory_id: IDL.Opt(IDL.Principal), + providers: IDL.Vec(IDL.Tuple(OpenIdAutomationProvider, OpenIdAutomationProviderConfig)) + }); + const AutomationConfig = IDL.Record({ + updated_at: IDL.Opt(IDL.Nat64), + openid: IDL.Opt(AutomationConfigOpenId), + created_at: IDL.Opt(IDL.Nat64), + version: IDL.Opt(IDL.Nat64) + }); const ConfigMaxMemorySize = IDL.Record({ stable: IDL.Opt(IDL.Nat64), heap: IDL.Opt(IDL.Nat64) @@ -231,7 +288,8 @@ export const idlFactory = ({ IDL }) => { const Config = IDL.Record({ db: IDL.Opt(DbConfig), authentication: IDL.Opt(AuthenticationConfig), - storage: StorageConfig + storage: StorageConfig, + automation: IDL.Opt(AutomationConfig) }); const OpenIdGetDelegationArgs = IDL.Record({ jwt: IDL.Text, @@ -404,6 +462,10 @@ export const idlFactory = ({ IDL }) => { internet_identity: IDL.Opt(AuthenticationConfigInternetIdentity), rules: IDL.Opt(AuthenticationRules) }); + const SetAutomationConfig = IDL.Record({ + openid: IDL.Opt(AutomationConfigOpenId), + version: IDL.Opt(IDL.Nat64) + }); const SetController = IDL.Record({ metadata: IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text)), kind: IDL.Opt(ControllerKind), @@ -452,6 +514,11 @@ export const idlFactory = ({ IDL }) => { return IDL.Service({ authenticate: IDL.Func([AuthenticationArgs], [AuthenticateResultResponse], []), + authenticate_automation: IDL.Func( + [AuthenticateAutomationArgs], + [AuthenticateAutomationResultResponse], + [] + ), commit_asset_upload: IDL.Func([CommitBatch], [], []), commit_proposal: IDL.Func([CommitProposal], [IDL.Null], []), commit_proposal_asset_upload: IDL.Func([CommitBatch], [], []), @@ -480,6 +547,7 @@ export const idlFactory = ({ IDL }) => { deposit_cycles: IDL.Func([DepositCyclesArgs], [], []), get_asset: IDL.Func([IDL.Text, IDL.Text], [IDL.Opt(AssetNoContent)], []), get_auth_config: IDL.Func([], [IDL.Opt(AuthenticationConfig)], []), + get_automation_config: IDL.Func([], [IDL.Opt(AutomationConfig)], []), get_config: IDL.Func([], [Config], []), get_db_config: IDL.Func([], [IDL.Opt(DbConfig)], []), get_delegation: IDL.Func([GetDelegationArgs], [GetDelegationResultResponse], []), @@ -521,6 +589,7 @@ export const idlFactory = ({ IDL }) => { reject_proposal: IDL.Func([CommitProposal], [IDL.Null], []), set_asset_token: IDL.Func([IDL.Text, IDL.Text, IDL.Opt(IDL.Text)], [], []), set_auth_config: IDL.Func([SetAuthenticationConfig], [AuthenticationConfig], []), + set_automation_config: IDL.Func([SetAutomationConfig], [AutomationConfig], []), set_controllers: IDL.Func( [SetControllersArgs], [IDL.Vec(IDL.Tuple(IDL.Principal, Controller))], diff --git a/src/declarations/sputnik/sputnik.factory.did.js b/src/declarations/sputnik/sputnik.factory.did.js index 74a3f16d91..ea3bbd4157 100644 --- a/src/declarations/sputnik/sputnik.factory.did.js +++ b/src/declarations/sputnik/sputnik.factory.did.js @@ -75,6 +75,40 @@ export const idlFactory = ({ IDL }) => { Ok: Authentication, Err: AuthenticationError }); + const OpenIdPrepareAutomationArgs = IDL.Record({ + jwt: IDL.Text, + salt: IDL.Vec(IDL.Nat8) + }); + const AuthenticateAutomationArgs = IDL.Variant({ + OpenId: OpenIdPrepareAutomationArgs + }); + const AutomationScope = IDL.Variant({ + Write: IDL.Null, + Submit: IDL.Null + }); + const AutomationController = IDL.Record({ + scope: AutomationScope, + expires_at: IDL.Nat64 + }); + const PrepareAutomationError = IDL.Variant({ + JwtFindProvider: JwtFindProviderError, + InvalidController: IDL.Text, + GetCachedJwks: IDL.Null, + JwtVerify: JwtVerifyError, + GetOrFetchJwks: GetOrRefreshJwksError, + ControllerAlreadyExists: IDL.Null, + TooManyControllers: IDL.Text + }); + const AuthenticationAutomationError = IDL.Variant({ + PrepareAutomation: PrepareAutomationError, + RegisterController: IDL.Text, + SaveWorkflowMetadata: IDL.Text, + SaveUniqueJtiToken: IDL.Text + }); + const AuthenticateAutomationResultResponse = IDL.Variant({ + Ok: IDL.Tuple(IDL.Principal, AutomationController), + Err: AuthenticationAutomationError + }); const CommitBatch = IDL.Record({ batch_id: IDL.Nat, headers: IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text)), @@ -194,6 +228,29 @@ export const idlFactory = ({ IDL }) => { internet_identity: IDL.Opt(AuthenticationConfigInternetIdentity), rules: IDL.Opt(AuthenticationRules) }); + const OpenIdAutomationProvider = IDL.Variant({ GitHub: IDL.Null }); + const OpenIdAutomationProviderControllerConfig = IDL.Record({ + scope: IDL.Opt(AutomationScope), + max_time_to_live: IDL.Opt(IDL.Nat64) + }); + const RepositoryKey = IDL.Record({ owner: IDL.Text, name: IDL.Text }); + const OpenIdAutomationRepositoryConfig = IDL.Record({ + branches: IDL.Opt(IDL.Vec(IDL.Text)) + }); + const OpenIdAutomationProviderConfig = IDL.Record({ + controller: IDL.Opt(OpenIdAutomationProviderControllerConfig), + repositories: IDL.Vec(IDL.Tuple(RepositoryKey, OpenIdAutomationRepositoryConfig)) + }); + const AutomationConfigOpenId = IDL.Record({ + observatory_id: IDL.Opt(IDL.Principal), + providers: IDL.Vec(IDL.Tuple(OpenIdAutomationProvider, OpenIdAutomationProviderConfig)) + }); + const AutomationConfig = IDL.Record({ + updated_at: IDL.Opt(IDL.Nat64), + openid: IDL.Opt(AutomationConfigOpenId), + created_at: IDL.Opt(IDL.Nat64), + version: IDL.Opt(IDL.Nat64) + }); const ConfigMaxMemorySize = IDL.Record({ stable: IDL.Opt(IDL.Nat64), heap: IDL.Opt(IDL.Nat64) @@ -231,7 +288,8 @@ export const idlFactory = ({ IDL }) => { const Config = IDL.Record({ db: IDL.Opt(DbConfig), authentication: IDL.Opt(AuthenticationConfig), - storage: StorageConfig + storage: StorageConfig, + automation: IDL.Opt(AutomationConfig) }); const OpenIdGetDelegationArgs = IDL.Record({ jwt: IDL.Text, @@ -404,6 +462,10 @@ export const idlFactory = ({ IDL }) => { internet_identity: IDL.Opt(AuthenticationConfigInternetIdentity), rules: IDL.Opt(AuthenticationRules) }); + const SetAutomationConfig = IDL.Record({ + openid: IDL.Opt(AutomationConfigOpenId), + version: IDL.Opt(IDL.Nat64) + }); const SetController = IDL.Record({ metadata: IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text)), kind: IDL.Opt(ControllerKind), @@ -452,6 +514,11 @@ export const idlFactory = ({ IDL }) => { return IDL.Service({ authenticate: IDL.Func([AuthenticationArgs], [AuthenticateResultResponse], []), + authenticate_automation: IDL.Func( + [AuthenticateAutomationArgs], + [AuthenticateAutomationResultResponse], + [] + ), commit_asset_upload: IDL.Func([CommitBatch], [], []), commit_proposal: IDL.Func([CommitProposal], [IDL.Null], []), commit_proposal_asset_upload: IDL.Func([CommitBatch], [], []), @@ -480,6 +547,7 @@ export const idlFactory = ({ IDL }) => { deposit_cycles: IDL.Func([DepositCyclesArgs], [], []), get_asset: IDL.Func([IDL.Text, IDL.Text], [IDL.Opt(AssetNoContent)], ['query']), get_auth_config: IDL.Func([], [IDL.Opt(AuthenticationConfig)], ['query']), + get_automation_config: IDL.Func([], [IDL.Opt(AutomationConfig)], ['query']), get_config: IDL.Func([], [Config], []), get_db_config: IDL.Func([], [IDL.Opt(DbConfig)], ['query']), get_delegation: IDL.Func([GetDelegationArgs], [GetDelegationResultResponse], ['query']), @@ -521,6 +589,7 @@ export const idlFactory = ({ IDL }) => { reject_proposal: IDL.Func([CommitProposal], [IDL.Null], []), set_asset_token: IDL.Func([IDL.Text, IDL.Text, IDL.Opt(IDL.Text)], [], []), set_auth_config: IDL.Func([SetAuthenticationConfig], [AuthenticationConfig], []), + set_automation_config: IDL.Func([SetAutomationConfig], [AutomationConfig], []), set_controllers: IDL.Func( [SetControllersArgs], [IDL.Vec(IDL.Tuple(IDL.Principal, Controller))], 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..2f8f284a3c --- /dev/null +++ b/src/libs/auth/src/automation/impls.rs @@ -0,0 +1,27 @@ +use crate::automation::types::{AutomationScope, PrepareAutomationError}; +use crate::openid::credentials::types::errors::VerifyOpenidCredentialsError; +use junobuild_shared::types::state::ControllerScope; + +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), + } + } +} + +impl From for ControllerScope { + fn from(scope: AutomationScope) -> Self { + match scope { + AutomationScope::Write => ControllerScope::Write, + AutomationScope::Submit => ControllerScope::Submit, + } + } +} diff --git a/src/libs/auth/src/automation/mod.rs b/src/libs/auth/src/automation/mod.rs new file mode 100644 index 0000000000..4db9a9240a --- /dev/null +++ b/src/libs/auth/src/automation/mod.rs @@ -0,0 +1,7 @@ +mod constants; +mod impls; +mod prepare; +pub mod types; +mod utils; + +pub use prepare::*; diff --git a/src/libs/auth/src/automation/prepare.rs b/src/libs/auth/src/automation/prepare.rs new file mode 100644 index 0000000000..b122c73cfe --- /dev/null +++ b/src/libs/auth/src/automation/prepare.rs @@ -0,0 +1,47 @@ +use crate::automation::types::{ + AutomationController, PrepareAutomationError, PrepareAutomationResult, PreparedAutomation, +}; +use crate::automation::utils::duration::build_expiration; +use crate::automation::utils::scope::build_scope; +use crate::openid::types::provider::OpenIdAutomationProvider; +use crate::strategies::{AuthAutomationStrategy, AuthHeapStrategy}; +use junobuild_shared::ic::api::caller; +use junobuild_shared::segments::controllers::{ + assert_controllers, assert_max_number_of_controllers, +}; +use junobuild_shared::types::state::ControllerId; + +pub fn openid_prepare_automation( + provider: &OpenIdAutomationProvider, + auth_heap: &impl AuthHeapStrategy, + auth_automation: &impl AuthAutomationStrategy, +) -> PrepareAutomationResult { + let controller_id = caller(); + + let existing_controllers = auth_automation.get_controllers(); + + if existing_controllers.contains_key(&controller_id) { + return Err(PrepareAutomationError::ControllerAlreadyExists); + } + + let submitted_controllers: [ControllerId; 1] = [controller_id]; + + assert_controllers(&submitted_controllers) + .map_err(PrepareAutomationError::InvalidController)?; + + let scope = build_scope(provider, auth_heap); + + assert_max_number_of_controllers( + &existing_controllers, + &submitted_controllers, + &scope.clone().into(), + None, + ) + .map_err(PrepareAutomationError::TooManyControllers)?; + + let expires_at = build_expiration(provider, auth_heap); + + let controller: AutomationController = AutomationController { expires_at, scope }; + + Ok(PreparedAutomation(controller_id, controller)) +} diff --git a/src/libs/auth/src/automation/types.rs b/src/libs/auth/src/automation/types.rs new file mode 100644 index 0000000000..68ffc87d79 --- /dev/null +++ b/src/libs/auth/src/automation/types.rs @@ -0,0 +1,40 @@ +use crate::openid::jwkset::types::errors::GetOrRefreshJwksError; +use crate::openid::jwt::types::errors::{JwtFindProviderError, JwtVerifyError}; +use crate::state::types::state::Salt; +use candid::{CandidType, Deserialize}; +use junobuild_shared::types::state::ControllerId; +use serde::Serialize; + +#[derive(CandidType, Serialize, Deserialize)] +pub struct OpenIdPrepareAutomationArgs { + pub jwt: String, + pub salt: Salt, +} + +pub type PrepareAutomationResult = Result; + +#[derive(CandidType, Serialize, Deserialize)] +pub struct PreparedAutomation(pub ControllerId, pub AutomationController); + +#[derive(CandidType, Serialize, Deserialize)] +pub struct AutomationController { + 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 { + ControllerAlreadyExists, + InvalidController(String), + TooManyControllers(String), + GetOrFetchJwks(GetOrRefreshJwksError), + GetCachedJwks, + JwtFindProvider(JwtFindProviderError), + JwtVerify(JwtVerifyError), +} 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..3f52d1de62 --- /dev/null +++ b/src/libs/auth/src/automation/utils/mod.rs @@ -0,0 +1,2 @@ +pub mod duration; +pub mod scope; 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..2a5053ffc4 --- /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) +} diff --git a/src/libs/auth/src/lib.rs b/src/libs/auth/src/lib.rs index 968d88c334..f753a00059 100644 --- a/src/libs/auth/src/lib.rs +++ b/src/libs/auth/src/lib.rs @@ -1,3 +1,4 @@ +pub mod automation; pub mod delegation; pub mod openid; pub mod profile; diff --git a/src/libs/auth/src/openid/credentials/automation/impls.rs b/src/libs/auth/src/openid/credentials/automation/impls.rs new file mode 100644 index 0000000000..22cab64769 --- /dev/null +++ b/src/libs/auth/src/openid/credentials/automation/impls.rs @@ -0,0 +1,26 @@ +use crate::openid::credentials::automation::types::interface::OpenIdAutomationCredential; +use crate::openid::credentials::automation::types::token::AutomationClaims; +use crate::openid::jwt::types::token::JwtClaims; +use jsonwebtoken::TokenData; + +impl From> for OpenIdAutomationCredential { + fn from(token: TokenData) -> Self { + Self { + sub: token.claims.sub, + iss: token.claims.iss, + jti: token.claims.jti, + repository: token.claims.repository, + repository_owner: token.claims.repository_owner, + r#ref: token.claims.r#ref, + run_id: token.claims.run_id, + run_number: token.claims.run_number, + run_attempt: token.claims.run_attempt, + } + } +} + +impl JwtClaims for AutomationClaims { + fn iat(&self) -> Option { + self.iat + } +} 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..35c6668fcf --- /dev/null +++ b/src/libs/auth/src/openid/credentials/automation/mod.rs @@ -0,0 +1,5 @@ +mod impls; +pub mod types; +mod verify; + +pub use verify::*; diff --git a/src/libs/auth/src/openid/credentials/automation/types.rs b/src/libs/auth/src/openid/credentials/automation/types.rs new file mode 100644 index 0000000000..9dc9997d0f --- /dev/null +++ b/src/libs/auth/src/openid/credentials/automation/types.rs @@ -0,0 +1,39 @@ +pub mod interface { + #[derive(Debug)] + pub struct OpenIdAutomationCredential { + pub iss: String, + pub sub: String, + pub jti: Option, + + // See https://docs.github.com/en/actions/concepts/security/openid-connect#understanding-the-oidc-token + pub repository: Option, // "octo-org/octo-repo" + pub repository_owner: Option, // "octo-org" + pub r#ref: Option, // "refs/heads/main" + pub run_id: Option, // "example-run-id" + pub run_number: Option, // 10" + pub run_attempt: Option, // "2" + } +} + +pub(crate) mod token { + use candid::Deserialize; + use serde::Serialize; + + #[derive(Debug, Clone, Deserialize, Serialize)] + pub struct AutomationClaims { + pub iss: String, + pub sub: String, + pub aud: String, + pub exp: Option, + pub nbf: Option, + pub iat: Option, + pub jti: Option, + + pub repository: Option, + pub repository_owner: Option, + pub r#ref: Option, + pub run_id: Option, + pub run_number: Option, + pub run_attempt: Option, + } +} 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..1f9963968d --- /dev/null +++ b/src/libs/auth/src/openid/credentials/automation/verify.rs @@ -0,0 +1,446 @@ +use crate::openid::credentials::automation::types::interface::OpenIdAutomationCredential; +use crate::openid::credentials::automation::types::token::AutomationClaims; +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::{unsafe_find_jwt_provider, verify_openid_jwt}; +use crate::openid::types::provider::{OpenIdAutomationProvider, OpenIdProvider}; +use crate::state::types::automation::{ + OpenIdAutomationProviderConfig, OpenIdAutomationProviders, RepositoryKey, +}; +use crate::state::types::state::Salt; +use crate::strategies::AuthHeapStrategy; + +type VerifyOpenIdAutomationCredentialsResult = + Result<(OpenIdAutomationCredential, OpenIdAutomationProvider), VerifyOpenidCredentialsError>; + +/// Verifies automation OIDC credentials (e.g. GitHub Actions) and returns the credential. +/// +/// ⚠️ **Warning:** This function does NOT enforce replay protection via JTI tracking. +/// +/// The caller MUST implement a replay protection. For example: +/// - Checking if the `jti` claim has been used before +/// - Storing the `jti` after successful verification +/// - Rejecting tokens with duplicate `jti` values +/// +/// In the Satellite implementation, this is handled by `save_unique_token_jti()`. +pub async fn verify_openid_credentials_with_jwks_renewal( + jwt: &str, + salt: &Salt, + 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, config, salt) +} + +fn verify_openid_credentials( + jwt: &str, + jwks: &Jwks, + provider: &OpenIdAutomationProvider, + config: &OpenIdAutomationProviderConfig, + salt: &Salt, +) -> VerifyOpenIdAutomationCredentialsResult { + let assert_nonce = |claims: &AutomationClaims, nonce: &String| -> Result<(), JwtVerifyError> { + // Ensure the JWT has not been intercepted and submitted with a different identity. + // We verify the audience matches the caller's principal + salt (GitHub does not allow customizing + // other JWT fields, making audience our only option for binding the JWT to a specific principal). + if claims.aud != nonce.as_str() { + return Err(JwtVerifyError::BadClaim("aud".to_string())); + } + + Ok(()) + }; + + let assert_repository = |claims: &AutomationClaims| -> Result<(), JwtVerifyError> { + let repository = claims + .repository + .as_ref() + .ok_or_else(|| JwtVerifyError::BadClaim("repository".to_string()))?; + + let parts: Vec<&str> = repository.split('/').collect(); + if parts.len() != 2 { + return Err(JwtVerifyError::BadClaim("repository_format".to_string())); + } + + let repo_key = RepositoryKey { + owner: parts[0].to_string(), + name: parts[1].to_string(), + }; + + let repo_config = config + .repositories + .get(&repo_key) + .ok_or_else(|| JwtVerifyError::BadClaim("repository_unauthorized".to_string()))?; + + if let Some(allowed_branches) = &repo_config.branches { + let ref_claim = claims + .r#ref + .as_ref() + .ok_or_else(|| JwtVerifyError::BadClaim("ref".to_string()))?; + + // ref is like "refs/heads/main", extract branch name + let branch = ref_claim + .strip_prefix("refs/heads/") + .ok_or_else(|| JwtVerifyError::BadClaim("ref_format".to_string()))?; + + if !allowed_branches.contains(&branch.to_string()) { + return Err(JwtVerifyError::BadClaim("branch_unauthorized".to_string())); + } + } + + Ok(()) + }; + + let token = verify_openid_jwt( + jwt, + provider.issuers(), + &jwks.keys, + &salt, + assert_nonce, + assert_repository, + ) + .map_err(VerifyOpenidCredentialsError::JwtVerify)?; + + let credential = OpenIdAutomationCredential::from(token); + + Ok((credential, provider.clone())) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::openid::jwt::types::cert::{Jwk, JwkParams, JwkParamsRsa, JwkType, Jwks}; + use crate::openid::types::provider::OpenIdAutomationProvider; + use crate::openid::utils::nonce::build_nonce; + use crate::state::types::automation::{ + OpenIdAutomationProviderConfig, OpenIdAutomationRepositories, + OpenIdAutomationRepositoryConfig, RepositoryKey, + }; + use crate::state::types::state::Salt; + use jsonwebtoken::{encode, Algorithm, EncodingKey, Header}; + use std::collections::HashMap; + use std::time::{SystemTime, UNIX_EPOCH}; + + const TEST_RSA_PEM: &str = include_str!("../../../../tests/keys/test_rsa.pem"); + const N_B64URL: &str = "qtQHkWpyd489-_bWjRtrvlQX9CwiQreOsi6kNeeySznI8u-8sxyuO3spW1r2pRmu-rc4jnD9vY6eTGZ3WFNIMxe1geXsF_3nQc5fcNJUUZj19BZE4Ud3dCmUQ4ezkslTvBj8RgD-iBJL7BT7YpxpPgvmqQy_9IgYUkDW4I9_e6kME5kVpySvpRznlk73PfAaDkHWmUTN0j2WcxkW09SGJ_f-tStaYXtc4uH5J-PWMRjwsfL66A_sxLxAwUODJ0VUbeDxVFHGJa0L-58_6GYDTqeel1vH4XjezDL8lf53YRyva3aFxGrC_JeLuIUaJOJX1hXWQb2DruB4hVcQX9afrQ"; + const E_B64URL: &str = "AQAB"; + const KID: &str = "test-kid"; + const ISS_GITHUB_ACTIONS: &str = "https://token.actions.githubusercontent.com"; + + fn now_secs() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() + } + + fn test_salt() -> Salt { + [42u8; 32] + } + + fn test_jwks() -> Jwks { + Jwks { + keys: vec![Jwk { + kty: JwkType::Rsa, + alg: Some("RS256".into()), + kid: Some(KID.into()), + params: JwkParams::Rsa(JwkParamsRsa { + n: N_B64URL.into(), + e: E_B64URL.into(), + }), + }], + } + } + + fn test_config() -> OpenIdAutomationProviderConfig { + let mut repositories: OpenIdAutomationRepositories = HashMap::new(); + + repositories.insert( + RepositoryKey { + owner: "octo-org".to_string(), + name: "octo-repo".to_string(), + }, + OpenIdAutomationRepositoryConfig { + branches: Some(vec!["main".to_string(), "develop".to_string()]), + }, + ); + + OpenIdAutomationProviderConfig { + repositories, + controller: None, + } + } + + fn create_token(claims: &AutomationClaims) -> String { + let mut header = Header::new(Algorithm::RS256); + header.kid = Some(KID.into()); + header.typ = Some("JWT".into()); + + let key = EncodingKey::from_rsa_pem(TEST_RSA_PEM.as_bytes()).unwrap(); + encode(&header, claims, &key).unwrap() + } + + #[test] + fn verifies_valid_automation_credentials() { + let now = now_secs(); + let salt = test_salt(); + let nonce = build_nonce(&salt); + + let claims = AutomationClaims { + iss: ISS_GITHUB_ACTIONS.into(), + sub: "repo:octo-org/octo-repo:ref:refs/heads/main".into(), + aud: nonce.clone(), + iat: Some(now), + exp: Some(now + 600), + nbf: None, + jti: Some("example-id".into()), + repository: Some("octo-org/octo-repo".into()), + repository_owner: Some("octo-org".into()), + r#ref: Some("refs/heads/main".into()), + run_id: Some("123456".into()), + run_number: Some("1".into()), + run_attempt: Some("1".into()), + }; + + let jwt = create_token(&claims); + let jwks = test_jwks(); + let config = test_config(); + + let result = verify_openid_credentials( + &jwt, + &jwks, + &OpenIdAutomationProvider::GitHub, + &config, + &salt, + ); + + assert!(result.is_ok()); + let (credential, provider) = result.unwrap(); + assert_eq!(provider, OpenIdAutomationProvider::GitHub); + assert_eq!(credential.repository.as_deref(), Some("octo-org/octo-repo")); + assert_eq!(credential.r#ref.as_deref(), Some("refs/heads/main")); + } + + #[test] + fn rejects_mismatched_audience() { + let now = now_secs(); + let salt = test_salt(); + + let claims = AutomationClaims { + iss: ISS_GITHUB_ACTIONS.into(), + sub: "repo:octo-org/octo-repo:ref:refs/heads/main".into(), + aud: "wrong-nonce".into(), + iat: Some(now), + exp: Some(now + 600), + nbf: None, + jti: Some("example-id".into()), + repository: Some("octo-org/octo-repo".into()), + repository_owner: Some("octo-org".into()), + r#ref: Some("refs/heads/main".into()), + run_id: Some("123456".into()), + run_number: Some("1".into()), + run_attempt: Some("1".into()), + }; + + let jwt = create_token(&claims); + let jwks = test_jwks(); + let config = test_config(); + + let result = verify_openid_credentials( + &jwt, + &jwks, + &OpenIdAutomationProvider::GitHub, + &config, + &salt, + ); + + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err(), + VerifyOpenidCredentialsError::JwtVerify(JwtVerifyError::BadClaim(ref c)) if c == "aud" + )); + } + + #[test] + fn rejects_unauthorized_repository() { + let now = now_secs(); + let salt = test_salt(); + let nonce = build_nonce(&salt); + + let claims = AutomationClaims { + iss: ISS_GITHUB_ACTIONS.into(), + sub: "repo:other-org/other-repo:ref:refs/heads/main".into(), + aud: nonce, + iat: Some(now), + exp: Some(now + 600), + nbf: None, + jti: Some("example-id".into()), + repository: Some("other-org/other-repo".into()), + repository_owner: Some("other-org".into()), + r#ref: Some("refs/heads/main".into()), + run_id: Some("123456".into()), + run_number: Some("1".into()), + run_attempt: Some("1".into()), + }; + + let jwt = create_token(&claims); + let jwks = test_jwks(); + let config = test_config(); + + let result = verify_openid_credentials( + &jwt, + &jwks, + &OpenIdAutomationProvider::GitHub, + &config, + &salt, + ); + + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err(), + VerifyOpenidCredentialsError::JwtVerify(JwtVerifyError::BadClaim(ref c)) if c == "repository_unauthorized" + )); + } + + #[test] + fn rejects_unauthorized_branch() { + let now = now_secs(); + let salt = test_salt(); + let nonce = build_nonce(&salt); + + let claims = AutomationClaims { + iss: ISS_GITHUB_ACTIONS.into(), + sub: "repo:octo-org/octo-repo:ref:refs/heads/feature".into(), + aud: nonce, + iat: Some(now), + exp: Some(now + 600), + nbf: None, + jti: Some("example-id".into()), + repository: Some("octo-org/octo-repo".into()), + repository_owner: Some("octo-org".into()), + r#ref: Some("refs/heads/feature".into()), + run_id: Some("123456".into()), + run_number: Some("1".into()), + run_attempt: Some("1".into()), + }; + + let jwt = create_token(&claims); + let jwks = test_jwks(); + let config = test_config(); + + let result = verify_openid_credentials( + &jwt, + &jwks, + &OpenIdAutomationProvider::GitHub, + &config, + &salt, + ); + + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err(), + VerifyOpenidCredentialsError::JwtVerify(JwtVerifyError::BadClaim(ref c)) if c == "branch_unauthorized" + )); + } + + #[test] + fn allows_all_branches_when_not_configured() { + let now = now_secs(); + let salt = test_salt(); + let nonce = build_nonce(&salt); + + let mut repositories: OpenIdAutomationRepositories = HashMap::new(); + repositories.insert( + RepositoryKey { + owner: "octo-org".to_string(), + name: "octo-repo".to_string(), + }, + OpenIdAutomationRepositoryConfig { branches: None }, + ); + + let config = OpenIdAutomationProviderConfig { + repositories, + controller: None, + }; + + let claims = AutomationClaims { + iss: ISS_GITHUB_ACTIONS.into(), + sub: "repo:octo-org/octo-repo:ref:refs/heads/any-branch".into(), + aud: nonce, + iat: Some(now), + exp: Some(now + 600), + nbf: None, + jti: Some("example-id".into()), + repository: Some("octo-org/octo-repo".into()), + repository_owner: Some("octo-org".into()), + r#ref: Some("refs/heads/any-branch".into()), + run_id: Some("123456".into()), + run_number: Some("1".into()), + run_attempt: Some("1".into()), + }; + + let jwt = create_token(&claims); + let jwks = test_jwks(); + + let result = verify_openid_credentials( + &jwt, + &jwks, + &OpenIdAutomationProvider::GitHub, + &config, + &salt, + ); + + assert!(result.is_ok()); + } + + #[test] + fn rejects_missing_repository_claim() { + let now = now_secs(); + let salt = test_salt(); + let nonce = build_nonce(&salt); + + let claims = AutomationClaims { + iss: ISS_GITHUB_ACTIONS.into(), + sub: "repo:octo-org/octo-repo:ref:refs/heads/main".into(), + aud: nonce, + iat: Some(now), + exp: Some(now + 600), + nbf: None, + jti: Some("example-id".into()), + repository: None, // Missing + repository_owner: Some("octo-org".into()), + r#ref: Some("refs/heads/main".into()), + run_id: Some("123456".into()), + run_number: Some("1".into()), + run_attempt: Some("1".into()), + }; + + let jwt = create_token(&claims); + let jwks = test_jwks(); + let config = test_config(); + + let result = verify_openid_credentials( + &jwt, + &jwks, + &OpenIdAutomationProvider::GitHub, + &config, + &salt, + ); + + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err(), + VerifyOpenidCredentialsError::JwtVerify(JwtVerifyError::BadClaim(ref c)) if c == "repository" + )); + } +} diff --git a/src/libs/auth/src/openid/credentials/mod.rs b/src/libs/auth/src/openid/credentials/mod.rs index 40a7344622..6da0e24a5d 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 automation; pub mod delegation; pub mod types; diff --git a/src/libs/auth/src/openid/impls.rs b/src/libs/auth/src/openid/impls.rs index 54e8387267..0b9f83073f 100644 --- a/src/libs/auth/src/openid/impls.rs +++ b/src/libs/auth/src/openid/impls.rs @@ -1,6 +1,8 @@ use crate::openid::jwt::types::cert::Jwks; use crate::openid::jwt::types::provider::JwtIssuers; -use crate::openid::types::provider::{OpenIdCertificate, OpenIdDelegationProvider, OpenIdProvider}; +use crate::openid::types::provider::{ + OpenIdAutomationProvider, OpenIdCertificate, OpenIdDelegationProvider, OpenIdProvider, +}; use junobuild_shared::data::version::next_version; use junobuild_shared::ic::api::time; use junobuild_shared::types::state::{Version, Versioned}; @@ -57,6 +59,42 @@ impl JwtIssuers for 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 OpenIdAutomationProvider { + fn issuers(&self) -> &[&'static str] { + self.issuers() + } +} + +impl Display for OpenIdAutomationProvider { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + match self { + OpenIdAutomationProvider::GitHub => write!(f, "GitHub"), + } + } +} + impl Versioned for OpenIdCertificate { fn version(&self) -> Option { self.version @@ -177,6 +215,30 @@ mod tests { ); } + #[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![] }; diff --git a/src/libs/auth/src/openid/types.rs b/src/libs/auth/src/openid/types.rs index 35b6b6f238..86606e1a7a 100644 --- a/src/libs/auth/src/openid/types.rs +++ b/src/libs/auth/src/openid/types.rs @@ -21,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/asserts/automation.rs b/src/libs/auth/src/state/asserts/automation.rs new file mode 100644 index 0000000000..8a1bf6815b --- /dev/null +++ b/src/libs/auth/src/state/asserts/automation.rs @@ -0,0 +1,24 @@ +use crate::state::types::automation::AutomationConfig; +use crate::state::types::interface::SetAutomationConfig; +use junobuild_shared::assert::assert_version; +use junobuild_shared::types::state::Version; + +pub fn assert_set_automation_config( + proposed_config: &SetAutomationConfig, + current_config: &Option, +) -> Result<(), String> { + assert_config_version(current_config, proposed_config.version)?; + + Ok(()) +} + +fn assert_config_version( + current_config: &Option, + proposed_version: Option, +) -> Result<(), String> { + if let Some(cfg) = current_config { + assert_version(proposed_version, cfg.version)? + } + + Ok(()) +} diff --git a/src/libs/auth/src/state/asserts/mod.rs b/src/libs/auth/src/state/asserts/mod.rs index bf3f94276e..90b545c98b 100644 --- a/src/libs/auth/src/state/asserts/mod.rs +++ b/src/libs/auth/src/state/asserts/mod.rs @@ -1 +1,5 @@ -pub mod authentication; +mod authentication; +mod automation; + +pub use authentication::*; +pub use automation::*; diff --git a/src/libs/auth/src/state/errors.rs b/src/libs/auth/src/state/errors.rs index 77c596c6ae..596b9aa08f 100644 --- a/src/libs/auth/src/state/errors.rs +++ b/src/libs/auth/src/state/errors.rs @@ -2,5 +2,8 @@ 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..333b7978f5 100644 --- a/src/libs/auth/src/state/heap.rs +++ b/src/libs/auth/src/state/heap.rs @@ -1,4 +1,5 @@ use crate::openid::types::provider::{OpenIdCertificate, OpenIdProvider}; +use crate::state::types::automation::AutomationConfig; use crate::state::types::config::AuthenticationConfig; use crate::state::types::state::Salt; use crate::state::types::state::{AuthenticationHeapState, OpenIdCachedCertificate, OpenIdState}; @@ -23,6 +24,7 @@ fn insert_config_impl(config: &AuthenticationConfig, state: &mut Option { *state = Some(AuthenticationHeapState { config: config.clone(), + automation: None, salt: None, openid: None, }) @@ -31,6 +33,40 @@ 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: &AutomationConfig) { + auth_heap + .with_auth_state_mut(|authentication| insert_automation_impl(automation, authentication)) +} + +fn insert_automation_impl( + automation: &AutomationConfig, + state: &mut Option, +) { + match state { + None => { + *state = Some(AuthenticationHeapState { + config: AuthenticationConfig::default(), + automation: Some(automation.clone()), + salt: None, + openid: None, + }) + } + Some(state) => state.automation = Some(automation.clone()), + } +} + // --------------------------------------------------------- // Salt // --------------------------------------------------------- @@ -48,6 +84,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/impls.rs b/src/libs/auth/src/state/impls.rs index bba2acce0f..80c7996bbb 100644 --- a/src/libs/auth/src/state/impls.rs +++ b/src/libs/auth/src/state/impls.rs @@ -1,6 +1,7 @@ use crate::openid::types::provider::OpenIdCertificate; +use crate::state::types::automation::AutomationConfig; use crate::state::types::config::AuthenticationConfig; -use crate::state::types::interface::SetAuthenticationConfig; +use crate::state::types::interface::{SetAuthenticationConfig, SetAutomationConfig}; use crate::state::types::state::{OpenIdCachedCertificate, OpenIdLastFetchAttempt}; use ic_cdk::api::time; use junobuild_shared::data::version::next_version; @@ -47,6 +48,37 @@ impl AuthenticationConfig { } } +impl AutomationConfig { + pub fn prepare( + current_config: &Option, + user_config: &SetAutomationConfig, + ) -> Self { + let now = time(); + + let created_at: Timestamp = match current_config { + None => now, + Some(current_doc) => current_doc.created_at.unwrap_or(now), + }; + + let version = next_version(current_config); + + let updated_at: Timestamp = now; + + AutomationConfig { + openid: user_config.openid.clone(), + created_at: Some(created_at), + updated_at: Some(updated_at), + version: Some(version), + } + } +} + +impl Versioned for AutomationConfig { + fn version(&self) -> Option { + self.version + } +} + impl OpenIdCachedCertificate { pub fn init() -> Self { Self { diff --git a/src/libs/auth/src/state/mod.rs b/src/libs/auth/src/state/mod.rs index b29efb91f8..6b7a343a71 100644 --- a/src/libs/auth/src/state/mod.rs +++ b/src/libs/auth/src/state/mod.rs @@ -9,8 +9,8 @@ mod store; pub mod types; pub use heap::{ - cache_certificate, get_cached_certificate, get_config, get_openid_state, get_salt, insert_salt, - record_fetch_attempt, + cache_certificate, get_automation, get_cached_certificate, get_config, get_openid_state, + get_salt, insert_salt, record_fetch_attempt, }; 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 1d427fc969..d5b43edf47 100644 --- a/src/libs/auth/src/state/store.rs +++ b/src/libs/auth/src/state/store.rs @@ -1,9 +1,13 @@ -use crate::errors::{JUNO_AUTH_ERROR_NOT_CONFIGURED, JUNO_AUTH_ERROR_OPENID_DISABLED}; -use crate::state::asserts::authentication::assert_set_authentication_config; -use crate::state::heap::get_config; -use crate::state::heap::insert_config; +use crate::errors::{ + JUNO_AUTH_ERROR_AUTOMATION_NOT_CONFIGURED, JUNO_AUTH_ERROR_NOT_CONFIGURED, + JUNO_AUTH_ERROR_OPENID_DISABLED, +}; +use crate::state::asserts::{assert_set_authentication_config, assert_set_automation_config}; +use crate::state::heap::{get_automation, get_config}; +use crate::state::heap::{insert_automation, insert_config}; +use crate::state::types::automation::{AutomationConfig, OpenIdAutomationProviders}; use crate::state::types::config::{AuthenticationConfig, OpenIdAuthProviders}; -use crate::state::types::interface::SetAuthenticationConfig; +use crate::state::types::interface::{SetAuthenticationConfig, SetAutomationConfig}; use crate::state::{get_salt, insert_salt}; use crate::strategies::AuthHeapStrategy; use junobuild_shared::ic::api::print; @@ -24,6 +28,21 @@ pub fn set_authentication_config( Ok(config) } +pub fn set_automation_config( + auth_heap: &impl AuthHeapStrategy, + proposed_config: &SetAutomationConfig, +) -> Result { + let current_config = get_automation(auth_heap); + + assert_set_automation_config(proposed_config, ¤t_config)?; + + let config = AutomationConfig::prepare(¤t_config, proposed_config); + + insert_automation(auth_heap, &config); + + Ok(config) +} + pub async fn init_salt(auth_heap: &impl AuthHeapStrategy) -> Result<(), String> { let existing_salt = get_salt(auth_heap); @@ -56,3 +75,15 @@ 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..0e2a9668fb 100644 --- a/src/libs/auth/src/state/types.rs +++ b/src/libs/auth/src/state/types.rs @@ -1,6 +1,7 @@ pub mod state { use crate::delegation::types::Timestamp; use crate::openid::types::provider::{OpenIdCertificate, OpenIdProvider}; + use crate::state::types::automation::AutomationConfig; use crate::state::types::config::AuthenticationConfig; use candid::CandidType; use serde::{Deserialize, Serialize}; @@ -10,7 +11,11 @@ pub mod state { #[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,7 +109,66 @@ pub mod config { } } +pub mod automation { + use crate::automation::types::AutomationScope; + use crate::openid::types::provider::OpenIdAutomationProvider; + use candid::{CandidType, Deserialize, Principal}; + use junobuild_shared::types::state::{Timestamp, Version}; + use serde::Serialize; + use std::collections::{BTreeMap, HashMap}; + + #[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::automation::AutomationConfigOpenId; use crate::state::types::config::{ AuthenticationConfigInternetIdentity, AuthenticationConfigOpenId, AuthenticationRules, }; @@ -119,4 +183,10 @@ pub mod interface { pub rules: Option, pub version: Option, } + + #[derive(Default, CandidType, Serialize, Deserialize, Clone)] + pub struct SetAutomationConfig { + pub openid: Option, + pub version: Option, + } } diff --git a/src/libs/auth/src/strategies.rs b/src/libs/auth/src/strategies.rs index 62e58a9768..a043e3e478 100644 --- a/src/libs/auth/src/strategies.rs +++ b/src/libs/auth/src/strategies.rs @@ -1,5 +1,6 @@ use crate::state::types::state::AuthenticationHeapState; use ic_certification::Hash; +use junobuild_shared::types::state::Controllers; pub trait AuthHeapStrategy { fn with_auth_state(&self, f: impl FnOnce(&Option) -> R) -> R; @@ -15,3 +16,7 @@ pub trait AuthCertificateStrategy { fn get_asset_hashes_root_hash(&self) -> Hash; } + +pub trait AuthAutomationStrategy { + fn get_controllers(&self) -> Controllers; +} diff --git a/src/libs/collections/src/constants/db.rs b/src/libs/collections/src/constants/db.rs index 79e29b9f00..d3dd96ac8e 100644 --- a/src/libs/collections/src/constants/db.rs +++ b/src/libs/collections/src/constants/db.rs @@ -8,6 +8,8 @@ pub const COLLECTION_LOG_KEY: &str = "#log"; pub const COLLECTION_USER_USAGE_KEY: &str = "#user-usage"; pub const COLLECTION_USER_WEBAUTHN_KEY: &str = "#user-webauthn"; pub const COLLECTION_USER_WEBAUTHN_INDEX_KEY: &str = "#user-webauthn-index"; +pub const COLLECTION_AUTOMATION_TOKEN_KEY: &str = "#automation-token"; +pub const COLLECTION_AUTOMATION_WORKFLOW_KEY: &str = "#automation-workflow"; const COLLECTION_USER_DEFAULT_RULE: SetRule = SetRule { read: Managed, @@ -76,7 +78,34 @@ pub const COLLECTION_USER_WEBAUTHN_INDEX_DEFAULT_RULE: SetRule = SetRule { rate_config: None, }; -pub const DEFAULT_DB_COLLECTIONS: [(&str, SetRule); 5] = [ +pub const COLLECTION_AUTOMATION_TOKEN_DEFAULT_RULE: SetRule = SetRule { + // Created and read through internal hooks. Write is restricted to Satellites themselves. + read: Controllers, + write: Controllers, + memory: Some(Memory::Stable), + mutable_permissions: Some(false), + max_size: None, + max_capacity: None, + max_changes_per_user: None, + version: None, + rate_config: None, +}; + +pub const COLLECTION_AUTOMATION_WORKFLOW_DEFAULT_RULE: SetRule = SetRule { + // Created through internal hooks. Write is restricted to Satellites themselves. + // Read allowed for controllers. + read: Controllers, + write: Controllers, + memory: Some(Memory::Stable), + mutable_permissions: Some(false), + max_size: None, + max_capacity: None, + max_changes_per_user: None, + version: None, + rate_config: None, +}; + +pub const DEFAULT_DB_COLLECTIONS: [(&str, SetRule); 7] = [ (COLLECTION_USER_KEY, COLLECTION_USER_DEFAULT_RULE), (COLLECTION_LOG_KEY, COLLECTION_LOG_DEFAULT_RULE), ( @@ -91,4 +120,12 @@ pub const DEFAULT_DB_COLLECTIONS: [(&str, SetRule); 5] = [ COLLECTION_USER_WEBAUTHN_INDEX_KEY, COLLECTION_USER_WEBAUTHN_INDEX_DEFAULT_RULE, ), + ( + COLLECTION_AUTOMATION_TOKEN_KEY, + COLLECTION_AUTOMATION_TOKEN_DEFAULT_RULE, + ), + ( + COLLECTION_AUTOMATION_WORKFLOW_KEY, + COLLECTION_AUTOMATION_WORKFLOW_DEFAULT_RULE, + ), ]; diff --git a/src/libs/satellite/satellite.did b/src/libs/satellite/satellite.did index c341c742a7..eaf9d604ea 100644 --- a/src/libs/satellite/satellite.did +++ b/src/libs/satellite/satellite.did @@ -20,12 +20,25 @@ type AssetNoContent = record { version : opt nat64; }; type AssetsUpgradeOptions = record { clear_existing_assets : opt bool }; +type AuthenticateAutomationArgs = variant { + OpenId : OpenIdPrepareAutomationArgs; +}; +type AuthenticateAutomationResultResponse = variant { + Ok : record { principal; AutomationController }; + Err : AuthenticationAutomationError; +}; type AuthenticateResultResponse = variant { Ok : Authentication; Err : AuthenticationError; }; type Authentication = record { doc : Doc; delegation : PreparedDelegation }; type AuthenticationArgs = variant { OpenId : OpenIdPrepareDelegationArgs }; +type AuthenticationAutomationError = variant { + PrepareAutomation : PrepareAutomationError; + RegisterController : text; + SaveWorkflowMetadata : text; + SaveUniqueJtiToken : text; +}; type AuthenticationConfig = record { updated_at : opt nat64; openid : opt AuthenticationConfigOpenId; @@ -47,6 +60,24 @@ type AuthenticationError = variant { RegisterUser : text; }; type AuthenticationRules = record { allowed_callers : vec principal }; +type AutomationConfig = record { + updated_at : opt nat64; + openid : opt AutomationConfigOpenId; + created_at : opt nat64; + version : opt nat64; +}; +type AutomationConfigOpenId = record { + observatory_id : opt principal; + providers : vec record { + OpenIdAutomationProvider; + OpenIdAutomationProviderConfig; + }; +}; +type AutomationController = record { + scope : AutomationScope; + expires_at : nat64; +}; +type AutomationScope = variant { Write; Submit }; type CollectionType = variant { Db; Storage }; type CommitBatch = record { batch_id : nat; @@ -58,6 +89,7 @@ type Config = record { db : opt DbConfig; authentication : opt AuthenticationConfig; storage : StorageConfig; + automation : opt AutomationConfig; }; type ConfigMaxMemorySize = record { stable : opt nat64; heap : opt nat64 }; type Controller = record { @@ -220,6 +252,16 @@ type OpenIdAuthProviderDelegationConfig = record { targets : opt vec principal; max_time_to_live : opt nat64; }; +type OpenIdAutomationProvider = variant { GitHub }; +type OpenIdAutomationProviderConfig = record { + controller : opt OpenIdAutomationProviderControllerConfig; + repositories : vec record { RepositoryKey; OpenIdAutomationRepositoryConfig }; +}; +type OpenIdAutomationProviderControllerConfig = record { + scope : opt AutomationScope; + max_time_to_live : opt nat64; +}; +type OpenIdAutomationRepositoryConfig = record { branches : opt vec text }; type OpenIdDelegationProvider = variant { GitHub; Google }; type OpenIdGetDelegationArgs = record { jwt : text; @@ -227,12 +269,22 @@ type OpenIdGetDelegationArgs = record { salt : blob; expiration : nat64; }; +type OpenIdPrepareAutomationArgs = record { jwt : text; salt : blob }; type OpenIdPrepareDelegationArgs = record { jwt : text; session_key : blob; salt : blob; }; type Permission = variant { Controllers; Private; Public; Managed }; +type PrepareAutomationError = variant { + JwtFindProvider : JwtFindProviderError; + InvalidController : text; + GetCachedJwks; + JwtVerify : JwtVerifyError; + GetOrFetchJwks : GetOrRefreshJwksError; + ControllerAlreadyExists; + TooManyControllers : text; +}; type PrepareDelegationError = variant { JwtFindProvider : JwtFindProviderError; GetCachedJwks; @@ -265,6 +317,7 @@ type ProposalType = variant { SegmentsDeployment : SegmentsDeploymentOptions; }; type RateConfig = record { max_tokens : nat64; time_per_token_ns : nat64 }; +type RepositoryKey = record { owner : text; name : text }; type Rule = record { max_capacity : opt nat32; memory : opt Memory; @@ -289,6 +342,10 @@ type SetAuthenticationConfig = record { internet_identity : opt AuthenticationConfigInternetIdentity; rules : opt AuthenticationRules; }; +type SetAutomationConfig = record { + openid : opt AutomationConfigOpenId; + version : opt nat64; +}; type SetController = record { metadata : vec record { text; text }; kind : opt ControllerKind; @@ -376,6 +433,9 @@ type UploadChunk = record { type UploadChunkResult = record { chunk_id : nat }; service : (InitSatelliteArgs) -> { authenticate : (AuthenticationArgs) -> (AuthenticateResultResponse); + authenticate_automation : (AuthenticateAutomationArgs) -> ( + AuthenticateAutomationResultResponse, + ); commit_asset_upload : (CommitBatch) -> (); commit_proposal : (CommitProposal) -> (null); commit_proposal_asset_upload : (CommitBatch) -> (); @@ -402,6 +462,7 @@ service : (InitSatelliteArgs) -> { deposit_cycles : (DepositCyclesArgs) -> (); get_asset : (text, text) -> (opt AssetNoContent) query; get_auth_config : () -> (opt AuthenticationConfig) query; + get_automation_config : () -> (opt AutomationConfig) query; get_config : () -> (Config); get_db_config : () -> (opt DbConfig) query; get_delegation : (GetDelegationArgs) -> (GetDelegationResultResponse) query; @@ -435,6 +496,7 @@ service : (InitSatelliteArgs) -> { reject_proposal : (CommitProposal) -> (null); set_asset_token : (text, text, opt text) -> (); set_auth_config : (SetAuthenticationConfig) -> (AuthenticationConfig); + set_automation_config : (SetAutomationConfig) -> (AutomationConfig); set_controllers : (SetControllersArgs) -> ( vec record { principal; Controller }, ); 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/config.rs b/src/libs/satellite/src/api/config.rs index 962db010ce..f41919b73d 100644 --- a/src/libs/satellite/src/api/config.rs +++ b/src/libs/satellite/src/api/config.rs @@ -4,14 +4,18 @@ use crate::assets::storage::store::{ use crate::auth::store::{ get_config as get_auth_config_store, set_config as set_auth_config_store, }; +use crate::automation::store::{ + get_config as get_automation_config_store, set_config as set_automation_config_store, +}; use crate::db::store::{ get_config_store as get_db_config_store, set_config_store as set_db_config_store, }; use crate::db::types::config::DbConfig; use crate::db::types::interface::SetDbConfig; use crate::types::interface::Config; +use junobuild_auth::state::types::automation::AutomationConfig; use junobuild_auth::state::types::config::AuthenticationConfig; -use junobuild_auth::state::types::interface::SetAuthenticationConfig; +use junobuild_auth::state::types::interface::{SetAuthenticationConfig, SetAutomationConfig}; use junobuild_shared::ic::UnwrapOrTrap; use junobuild_storage::types::config::StorageConfig; use junobuild_storage::types::interface::SetStorageConfig; @@ -24,11 +28,13 @@ pub fn get_config() -> Config { let storage = get_storage_config_store(); let db = get_db_config_store(); let authentication = get_auth_config_store(); + let automation = get_automation_config_store(); Config { storage, db, authentication, + automation, } } @@ -44,6 +50,18 @@ pub fn get_auth_config() -> Option { get_auth_config_store() } +// --------------------------------------------------------- +// Automation config +// --------------------------------------------------------- + +pub async fn set_automation_config(config: SetAutomationConfig) -> AutomationConfig { + set_automation_config_store(&config).await.unwrap_or_trap() +} + +pub fn get_automation_config() -> Option { + get_automation_config_store() +} + // --------------------------------------------------------- // Db config // --------------------------------------------------------- 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..d6c09a0048 --- /dev/null +++ b/src/libs/satellite/src/automation/authenticate.rs @@ -0,0 +1,39 @@ +use crate::auth::strategy_impls::AuthHeap; +use crate::automation::controllers::register::register_controller; +use crate::automation::prepare; +use crate::automation::token::save_unique_token_jti; +use crate::automation::types::{AuthenticateAutomationResult, AuthenticationAutomationError}; +use crate::automation::workflow::save_workflow_metadata; +use junobuild_auth::automation::types::OpenIdPrepareAutomationArgs; +use junobuild_auth::state::get_automation_providers; + +pub async fn openid_authenticate_automation( + args: &OpenIdPrepareAutomationArgs, +) -> Result { + let providers = get_automation_providers(&AuthHeap)?; + + let prepared_automation = prepare::openid_prepare_automation(args, &providers).await; + + let result = match prepared_automation { + Ok((automation, provider, credential)) => { + if let Err(err) = save_unique_token_jti(&automation, &provider, &credential) { + return Ok(Err(AuthenticationAutomationError::SaveUniqueJtiToken(err))); + } + + if let Err(err) = save_workflow_metadata(&provider, &credential) { + return Ok(Err(AuthenticationAutomationError::SaveWorkflowMetadata( + err, + ))); + } + + if let Err(err) = register_controller(&automation, &provider, &credential) { + return Ok(Err(AuthenticationAutomationError::RegisterController(err))); + } + + Ok(automation) + } + Err(err) => Err(AuthenticationAutomationError::PrepareAutomation(err)), + }; + + Ok(result) +} diff --git a/src/libs/satellite/src/automation/controllers/mod.rs b/src/libs/satellite/src/automation/controllers/mod.rs new file mode 100644 index 0000000000..f862bee245 --- /dev/null +++ b/src/libs/satellite/src/automation/controllers/mod.rs @@ -0,0 +1 @@ +pub mod register; diff --git a/src/libs/satellite/src/automation/controllers/register.rs b/src/libs/satellite/src/automation/controllers/register.rs new file mode 100644 index 0000000000..d568b6082c --- /dev/null +++ b/src/libs/satellite/src/automation/controllers/register.rs @@ -0,0 +1,33 @@ +use crate::automation::workflow::build_automation_workflow_key; +use crate::controllers::store::set_controllers; +use junobuild_auth::automation::types::PreparedAutomation; +use junobuild_auth::openid::credentials::automation::types::interface::OpenIdAutomationCredential; +use junobuild_auth::openid::types::provider::OpenIdAutomationProvider; +use junobuild_shared::types::interface::SetController; +use junobuild_shared::types::state::{ControllerId, ControllerKind, Metadata}; + +pub fn register_controller( + automation: &PreparedAutomation, + provider: &OpenIdAutomationProvider, + credential: &OpenIdAutomationCredential, +) -> Result<(), String> { + let PreparedAutomation(controller_id, controller) = automation; + + let controllers: [ControllerId; 1] = [controller_id.clone()]; + + let automation_workflow_key = build_automation_workflow_key(provider, credential)?; + + let mut metadata: Metadata = Default::default(); + metadata.insert("workflow_key".to_string(), automation_workflow_key.to_key()); + + let controller: SetController = SetController { + scope: controller.scope.clone().into(), + metadata, + expires_at: Some(controller.expires_at), + kind: Some(ControllerKind::Automation), + }; + + set_controllers(&controllers, &controller); + + Ok(()) +} diff --git a/src/libs/satellite/src/automation/mod.rs b/src/libs/satellite/src/automation/mod.rs new file mode 100644 index 0000000000..d03cd7c7cf --- /dev/null +++ b/src/libs/satellite/src/automation/mod.rs @@ -0,0 +1,11 @@ +pub mod authenticate; +mod controllers; +mod prepare; +pub mod store; +mod strategy_impls; +mod token; +pub mod types; +mod workflow; + +pub use token::assert::*; +pub use workflow::assert::*; diff --git a/src/libs/satellite/src/automation/prepare.rs b/src/libs/satellite/src/automation/prepare.rs new file mode 100644 index 0000000000..d8d13dfb39 --- /dev/null +++ b/src/libs/satellite/src/automation/prepare.rs @@ -0,0 +1,38 @@ +use crate::auth::strategy_impls::AuthHeap; +use crate::automation::strategy_impls::AuthAutomation; +use junobuild_auth::automation; +use junobuild_auth::automation::types::{ + OpenIdPrepareAutomationArgs, PrepareAutomationError, PreparedAutomation, +}; +use junobuild_auth::openid::credentials; +use junobuild_auth::openid::credentials::automation::types::interface::OpenIdAutomationCredential; +use junobuild_auth::openid::types::provider::OpenIdAutomationProvider; +use junobuild_auth::state::types::automation::OpenIdAutomationProviders; + +pub type OpenIdPrepareAutomationResult = Result< + ( + PreparedAutomation, + OpenIdAutomationProvider, + OpenIdAutomationCredential, + ), + PrepareAutomationError, +>; + +pub async fn openid_prepare_automation( + args: &OpenIdPrepareAutomationArgs, + providers: &OpenIdAutomationProviders, +) -> OpenIdPrepareAutomationResult { + let (credential, provider) = + match credentials::automation::verify_openid_credentials_with_jwks_renewal( + &args.jwt, &args.salt, providers, &AuthHeap, + ) + .await + { + Ok(value) => value, + Err(err) => return Err(PrepareAutomationError::from(err)), + }; + + let result = automation::openid_prepare_automation(&provider, &AuthHeap, &AuthAutomation); + + result.map(|prepared_automation| (prepared_automation, provider, credential)) +} diff --git a/src/libs/satellite/src/automation/store.rs b/src/libs/satellite/src/automation/store.rs new file mode 100644 index 0000000000..dfe6213531 --- /dev/null +++ b/src/libs/satellite/src/automation/store.rs @@ -0,0 +1,14 @@ +use crate::auth::strategy_impls::AuthHeap; +use junobuild_auth::state::types::automation::AutomationConfig; +use junobuild_auth::state::types::interface::SetAutomationConfig; +use junobuild_auth::state::{ + get_automation as get_state_automation, set_automation_config as set_store_automation_config, +}; + +pub async fn set_config(proposed_config: &SetAutomationConfig) -> Result { + set_store_automation_config(&AuthHeap, proposed_config) +} + +pub fn get_config() -> Option { + get_state_automation(&AuthHeap) +} diff --git a/src/libs/satellite/src/automation/strategy_impls.rs b/src/libs/satellite/src/automation/strategy_impls.rs new file mode 100644 index 0000000000..6f16284c64 --- /dev/null +++ b/src/libs/satellite/src/automation/strategy_impls.rs @@ -0,0 +1,11 @@ +use crate::get_controllers; +use junobuild_auth::strategies::AuthAutomationStrategy; +use junobuild_shared::types::state::Controllers; + +pub struct AuthAutomation; + +impl AuthAutomationStrategy for AuthAutomation { + fn get_controllers(&self) -> Controllers { + get_controllers() + } +} diff --git a/src/libs/satellite/src/automation/token/assert.rs b/src/libs/satellite/src/automation/token/assert.rs new file mode 100644 index 0000000000..4f6c84ff57 --- /dev/null +++ b/src/libs/satellite/src/automation/token/assert.rs @@ -0,0 +1,21 @@ +use crate::errors::automation::JUNO_DATASTORE_ERROR_AUTOMATION_CALLER; +use candid::Principal; +use junobuild_collections::constants::db::COLLECTION_AUTOMATION_TOKEN_KEY; +use junobuild_collections::types::core::CollectionKey; +use junobuild_shared::ic::api::id; +use junobuild_shared::utils::principal_not_equal; + +pub fn assert_automation_token_caller( + caller: Principal, + collection: &CollectionKey, +) -> Result<(), String> { + if collection != COLLECTION_AUTOMATION_TOKEN_KEY { + return Ok(()); + } + + if principal_not_equal(id(), caller) { + return Err(JUNO_DATASTORE_ERROR_AUTOMATION_CALLER.to_string()); + } + + Ok(()) +} diff --git a/src/libs/satellite/src/automation/token/impls.rs b/src/libs/satellite/src/automation/token/impls.rs new file mode 100644 index 0000000000..923aafa618 --- /dev/null +++ b/src/libs/satellite/src/automation/token/impls.rs @@ -0,0 +1,34 @@ +use crate::automation::token::types::state::{AutomationTokenData, AutomationTokenKey}; +use crate::{Doc, SetDoc}; +use junobuild_auth::openid::types::provider::OpenIdAutomationProvider; +use junobuild_utils::encode_doc_data; + +impl AutomationTokenKey { + pub fn create(provider: &OpenIdAutomationProvider, jti: &str) -> Self { + Self { + provider: provider.clone(), + jti: jti.to_owned(), + } + } + + pub fn to_key(&self) -> String { + format!("{}#{}", self.provider, self.jti) + } +} + +impl AutomationTokenData { + pub fn prepare_set_doc( + token_data: &AutomationTokenData, + current_doc: &Option, + ) -> Result { + let data = encode_doc_data(token_data)?; + + let set_doc = SetDoc { + data, + description: None, + version: current_doc.as_ref().and_then(|d| d.version), + }; + + Ok(set_doc) + } +} diff --git a/src/libs/satellite/src/automation/token/mod.rs b/src/libs/satellite/src/automation/token/mod.rs new file mode 100644 index 0000000000..528a444196 --- /dev/null +++ b/src/libs/satellite/src/automation/token/mod.rs @@ -0,0 +1,6 @@ +pub mod assert; +mod impls; +mod services; +mod types; + +pub use services::*; diff --git a/src/libs/satellite/src/automation/token/services.rs b/src/libs/satellite/src/automation/token/services.rs new file mode 100644 index 0000000000..39fdc78c02 --- /dev/null +++ b/src/libs/satellite/src/automation/token/services.rs @@ -0,0 +1,71 @@ +use crate::automation::token::types::state::{AutomationTokenData, AutomationTokenKey}; +use crate::db::internal::unsafe_get_doc; +use crate::db::store::internal_set_doc_store; +use crate::db::types::store::AssertSetDocOptions; +use crate::errors::automation::{ + JUNO_AUTOMATION_TOKEN_ERROR_MISSING_JTI, JUNO_AUTOMATION_TOKEN_ERROR_TOKEN_REUSED, +}; +use crate::rules::store::get_rule_db; +use junobuild_auth::automation::types::PreparedAutomation; +use junobuild_auth::openid::credentials::automation::types::interface::OpenIdAutomationCredential; +use junobuild_auth::openid::types::provider::OpenIdAutomationProvider; +use junobuild_collections::constants::db::COLLECTION_AUTOMATION_TOKEN_KEY; +use junobuild_collections::msg::msg_db_collection_not_found; +use junobuild_shared::ic::api::id; +use junobuild_utils::DocDataPrincipal; + +pub fn save_unique_token_jti( + prepared_automation: &PreparedAutomation, + provider: &OpenIdAutomationProvider, + credential: &OpenIdAutomationCredential, +) -> Result<(), String> { + let jti = if let Some(jti) = &credential.jti { + jti + } else { + return Err(JUNO_AUTOMATION_TOKEN_ERROR_MISSING_JTI.to_string()); + }; + + let automation_token_key = AutomationTokenKey::create(provider, jti).to_key(); + + let automation_token_collection = COLLECTION_AUTOMATION_TOKEN_KEY.to_string(); + + let rule = get_rule_db(&automation_token_collection) + .ok_or_else(|| msg_db_collection_not_found(&automation_token_collection))?; + + let current_jti = unsafe_get_doc( + &automation_token_collection.to_string(), + &automation_token_key, + &rule, + )?; + + // ⚠️ Assertion to prevent replay attack. + if current_jti.is_some() { + return Err(JUNO_AUTOMATION_TOKEN_ERROR_TOKEN_REUSED.to_string()); + } + + // Create metadata. + let PreparedAutomation(controller_id, _) = prepared_automation; + + let automation_token_data: AutomationTokenData = AutomationTokenData { + controller_id: DocDataPrincipal { + value: controller_id.clone(), + }, + }; + + let automation_token_data = + AutomationTokenData::prepare_set_doc(&automation_token_data, &None)?; + + let assert_options = AssertSetDocOptions { + with_assert_rate: true, + }; + + internal_set_doc_store( + id(), + automation_token_collection, + automation_token_key, + automation_token_data, + &assert_options, + )?; + + Ok(()) +} diff --git a/src/libs/satellite/src/automation/token/types.rs b/src/libs/satellite/src/automation/token/types.rs new file mode 100644 index 0000000000..ae2ad1be86 --- /dev/null +++ b/src/libs/satellite/src/automation/token/types.rs @@ -0,0 +1,21 @@ +pub mod state { + use candid::Deserialize; + use junobuild_auth::openid::types::provider::OpenIdAutomationProvider; + use junobuild_utils::DocDataPrincipal; + use serde::Serialize; + + /// A unique key for identifying an automation token. + /// Used to prevent replay attack + /// The key will be parsed to `provider#jti`. + #[derive(Serialize, Deserialize)] + pub struct AutomationTokenKey { + pub provider: OpenIdAutomationProvider, + pub jti: String, + } + + #[derive(Serialize, Deserialize)] + #[serde(rename_all = "camelCase", deny_unknown_fields)] + pub struct AutomationTokenData { + pub controller_id: DocDataPrincipal, + } +} diff --git a/src/libs/satellite/src/automation/types.rs b/src/libs/satellite/src/automation/types.rs new file mode 100644 index 0000000000..33e5b921f7 --- /dev/null +++ b/src/libs/satellite/src/automation/types.rs @@ -0,0 +1,20 @@ +use candid::{CandidType, Deserialize}; +use junobuild_auth::automation::types::{ + OpenIdPrepareAutomationArgs, PrepareAutomationError, PreparedAutomation, +}; +use serde::Serialize; + +#[derive(CandidType, Serialize, Deserialize)] +pub enum AuthenticateAutomationArgs { + OpenId(OpenIdPrepareAutomationArgs), +} + +#[derive(CandidType, Serialize, Deserialize)] +pub enum AuthenticationAutomationError { + PrepareAutomation(PrepareAutomationError), + SaveUniqueJtiToken(String), + SaveWorkflowMetadata(String), + RegisterController(String), +} + +pub type AuthenticateAutomationResult = Result; diff --git a/src/libs/satellite/src/automation/workflow/assert.rs b/src/libs/satellite/src/automation/workflow/assert.rs new file mode 100644 index 0000000000..ef030f59f6 --- /dev/null +++ b/src/libs/satellite/src/automation/workflow/assert.rs @@ -0,0 +1,21 @@ +use crate::errors::automation::JUNO_DATASTORE_ERROR_AUTOMATION_CALLER; +use candid::Principal; +use junobuild_collections::constants::db::COLLECTION_AUTOMATION_WORKFLOW_KEY; +use junobuild_collections::types::core::CollectionKey; +use junobuild_shared::ic::api::id; +use junobuild_shared::utils::principal_not_equal; + +pub fn assert_automation_workflow_caller( + caller: Principal, + collection: &CollectionKey, +) -> Result<(), String> { + if collection != COLLECTION_AUTOMATION_WORKFLOW_KEY { + return Ok(()); + } + + if principal_not_equal(id(), caller) { + return Err(JUNO_DATASTORE_ERROR_AUTOMATION_CALLER.to_string()); + } + + Ok(()) +} diff --git a/src/libs/satellite/src/automation/workflow/impls.rs b/src/libs/satellite/src/automation/workflow/impls.rs new file mode 100644 index 0000000000..714a4872a7 --- /dev/null +++ b/src/libs/satellite/src/automation/workflow/impls.rs @@ -0,0 +1,35 @@ +use crate::automation::workflow::types::state::{AutomationWorkflowData, AutomationWorkflowKey}; +use crate::{Doc, SetDoc}; +use junobuild_auth::openid::types::provider::OpenIdAutomationProvider; +use junobuild_utils::encode_doc_data; + +impl AutomationWorkflowKey { + pub fn create(provider: &OpenIdAutomationProvider, repository: &str, run_id: &str) -> Self { + Self { + provider: provider.clone(), + repository: repository.to_owned(), + run_id: run_id.to_owned(), + } + } + + pub fn to_key(&self) -> String { + format!("{}#{}#{}", self.provider, self.repository, self.run_id) + } +} + +impl AutomationWorkflowData { + pub fn prepare_set_doc( + workflow_data: &AutomationWorkflowData, + current_doc: &Option, + ) -> Result { + let data = encode_doc_data(workflow_data)?; + + let set_doc = SetDoc { + data, + description: None, + version: current_doc.as_ref().and_then(|d| d.version), + }; + + Ok(set_doc) + } +} diff --git a/src/libs/satellite/src/automation/workflow/mod.rs b/src/libs/satellite/src/automation/workflow/mod.rs new file mode 100644 index 0000000000..551e81b6bc --- /dev/null +++ b/src/libs/satellite/src/automation/workflow/mod.rs @@ -0,0 +1,8 @@ +pub mod assert; +mod impls; +mod services; +mod types; +mod utils; + +pub use services::*; +pub use utils::*; diff --git a/src/libs/satellite/src/automation/workflow/services.rs b/src/libs/satellite/src/automation/workflow/services.rs new file mode 100644 index 0000000000..e42802aec9 --- /dev/null +++ b/src/libs/satellite/src/automation/workflow/services.rs @@ -0,0 +1,58 @@ +use crate::automation::workflow::types::state::AutomationWorkflowData; +use crate::automation::workflow::utils::build_automation_workflow_key; +use crate::db::internal::unsafe_get_doc; +use crate::db::store::internal_set_doc_store; +use crate::db::types::store::AssertSetDocOptions; +use crate::rules::store::get_rule_db; +use junobuild_auth::openid::credentials::automation::types::interface::OpenIdAutomationCredential; +use junobuild_auth::openid::types::provider::OpenIdAutomationProvider; +use junobuild_collections::constants::db::COLLECTION_AUTOMATION_WORKFLOW_KEY; +use junobuild_collections::msg::msg_db_collection_not_found; +use junobuild_shared::ic::api::id; + +pub fn save_workflow_metadata( + provider: &OpenIdAutomationProvider, + credential: &OpenIdAutomationCredential, +) -> Result<(), String> { + let automation_workflow_key = build_automation_workflow_key(provider, credential)?.to_key(); + + let automation_workflow_collection = COLLECTION_AUTOMATION_WORKFLOW_KEY.to_string(); + + let rule = get_rule_db(&automation_workflow_collection) + .ok_or_else(|| msg_db_collection_not_found(&automation_workflow_collection))?; + + let current_automation_workflow = unsafe_get_doc( + &automation_workflow_collection.to_string(), + &automation_workflow_key, + &rule, + )?; + + // Create or update metadata. Since we are "only" saving the latest information, we always + // update the fields. + let automation_workflow_data: AutomationWorkflowData = AutomationWorkflowData { + run_number: credential.run_number.clone(), + run_attempt: credential.run_attempt.clone(), + r#ref: credential.r#ref.clone(), + }; + + let automation_workflow_data = AutomationWorkflowData::prepare_set_doc( + &automation_workflow_data, + ¤t_automation_workflow, + )?; + + let assert_options = AssertSetDocOptions { + // We disable the assertion for the rate because it has been asserted + // before when saving the jti. + with_assert_rate: false, + }; + + internal_set_doc_store( + id(), + automation_workflow_collection, + automation_workflow_key, + automation_workflow_data, + &assert_options, + )?; + + Ok(()) +} diff --git a/src/libs/satellite/src/automation/workflow/types.rs b/src/libs/satellite/src/automation/workflow/types.rs new file mode 100644 index 0000000000..4f45c15da1 --- /dev/null +++ b/src/libs/satellite/src/automation/workflow/types.rs @@ -0,0 +1,24 @@ +pub mod state { + use candid::Deserialize; + use junobuild_auth::openid::types::provider::OpenIdAutomationProvider; + use serde::Serialize; + + /// A unique key for identifying an automation workflow. + /// The key will be parsed to `provider#repository#id`. + #[derive(Serialize, Deserialize)] + pub struct AutomationWorkflowKey { + pub provider: OpenIdAutomationProvider, + pub repository: String, + pub run_id: String, // e.g. run_id for GitHub, pipeline_id for GitLab + } + + /// Deployment workflow metadata. + /// Stores the latest state if a workflow has multiple attempts. + #[derive(Serialize, Deserialize)] + #[serde(rename_all = "camelCase", deny_unknown_fields)] + pub struct AutomationWorkflowData { + pub run_number: Option, // The number of times this workflow has been run. + pub run_attempt: Option, // The number of times this workflow run has been retried. + pub r#ref: Option, // (Reference) The latest git ref that triggered the workflow run. e.g. "refs/heads/main" + } +} diff --git a/src/libs/satellite/src/automation/workflow/utils.rs b/src/libs/satellite/src/automation/workflow/utils.rs new file mode 100644 index 0000000000..ed50122497 --- /dev/null +++ b/src/libs/satellite/src/automation/workflow/utils.rs @@ -0,0 +1,28 @@ +use crate::automation::workflow::types::state::AutomationWorkflowKey; +use crate::errors::automation::{ + JUNO_AUTOMATION_WORKFLOW_ERROR_MISSING_REPOSITORY, + JUNO_AUTOMATION_WORKFLOW_ERROR_MISSING_RUN_ID, +}; +use junobuild_auth::openid::credentials::automation::types::interface::OpenIdAutomationCredential; +use junobuild_auth::openid::types::provider::OpenIdAutomationProvider; + +pub fn build_automation_workflow_key( + provider: &OpenIdAutomationProvider, + credential: &OpenIdAutomationCredential, +) -> Result { + let repository = if let Some(repository) = &credential.repository { + repository + } else { + return Err(JUNO_AUTOMATION_WORKFLOW_ERROR_MISSING_REPOSITORY.to_string()); + }; + + let run_id = if let Some(run_id) = &credential.run_id { + run_id + } else { + return Err(JUNO_AUTOMATION_WORKFLOW_ERROR_MISSING_RUN_ID.to_string()); + }; + + let automation_workflow_key = AutomationWorkflowKey::create(provider, repository, run_id); + + Ok(automation_workflow_key) +} diff --git a/src/libs/satellite/src/db/assert.rs b/src/libs/satellite/src/db/assert.rs index c041f2b4b8..7d92c59b79 100644 --- a/src/libs/satellite/src/db/assert.rs +++ b/src/libs/satellite/src/db/assert.rs @@ -1,4 +1,5 @@ use crate::auth::assert::assert_caller_is_allowed; +use crate::automation::{assert_automation_token_caller, assert_automation_workflow_caller}; use crate::db::runtime::increment_and_assert_rate; use crate::db::types::config::DbConfig; use crate::db::types::interface::SetDbConfig; @@ -84,6 +85,10 @@ pub fn assert_set_doc( assert_user_webauthn_collection_data(caller, collection, value)?; assert_user_webauthn_collection_write_permission(collection, current_doc)?; + // Note: we do not assert the format of the automation keys or data since only the Satellite can write. + assert_automation_token_caller(caller, collection)?; + assert_automation_workflow_caller(caller, collection)?; + assert_write_permission(caller, controllers, current_doc, &rule.write)?; assert_memory_size(config)?; @@ -133,6 +138,9 @@ pub fn assert_delete_doc( assert_write_version(current_doc, value.version)?; + assert_automation_token_caller(caller, collection)?; + assert_automation_workflow_caller(caller, collection)?; + invoke_assert_delete_doc( &caller, &DocContext { diff --git a/src/libs/satellite/src/errors/automation.rs b/src/libs/satellite/src/errors/automation.rs new file mode 100644 index 0000000000..cb3d42db09 --- /dev/null +++ b/src/libs/satellite/src/errors/automation.rs @@ -0,0 +1,10 @@ +pub const JUNO_AUTOMATION_TOKEN_ERROR_MISSING_JTI: &str = "juno.automation.token.error.missing_jti"; +pub const JUNO_AUTOMATION_TOKEN_ERROR_TOKEN_REUSED: &str = + "juno.automation.token.error.token_reused"; + +pub const JUNO_AUTOMATION_WORKFLOW_ERROR_MISSING_REPOSITORY: &str = + "juno.automation.workflow.error.missing_repository"; +pub const JUNO_AUTOMATION_WORKFLOW_ERROR_MISSING_RUN_ID: &str = + "juno.automation.workflow.error.missing_run_id"; + +pub const JUNO_DATASTORE_ERROR_AUTOMATION_CALLER: &str = "juno.datastore.error.automation.caller"; diff --git a/src/libs/satellite/src/errors/mod.rs b/src/libs/satellite/src/errors/mod.rs index 005e7c4c52..9c2c666fce 100644 --- a/src/libs/satellite/src/errors/mod.rs +++ b/src/libs/satellite/src/errors/mod.rs @@ -1,3 +1,4 @@ pub mod auth; +pub mod automation; pub mod db; pub mod user; 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 9e3601de30..867020936a 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,13 +25,15 @@ use crate::guards::{ caller_is_admin_controller, caller_is_controller_with_write, caller_is_valid_controller, }; 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::automation::AutomationConfig; use junobuild_auth::state::types::config::AuthenticationConfig; -use junobuild_auth::state::types::interface::SetAuthenticationConfig; +use junobuild_auth::state::types::interface::{SetAuthenticationConfig, SetAutomationConfig}; use junobuild_cdn::proposals::{ CommitProposal, ListProposalResults, ListProposalsParams, Proposal, ProposalId, ProposalType, RejectProposal, @@ -176,6 +179,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 // --------------------------------------------------------- @@ -369,6 +380,22 @@ pub fn get_auth_config() -> Option { api::config::get_auth_config() } +// --------------------------------------------------------- +// Automation config +// --------------------------------------------------------- + +#[doc(hidden)] +#[update(guard = "caller_is_admin_controller")] +pub async fn set_automation_config(config: SetAutomationConfig) -> AutomationConfig { + api::config::set_automation_config(config).await +} + +#[doc(hidden)] +#[query(guard = "caller_is_admin_controller")] +pub fn get_automation_config() -> Option { + api::config::get_automation_config() +} + // --------------------------------------------------------- // Db config // --------------------------------------------------------- @@ -540,19 +567,20 @@ 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, - http_request, http_request_streaming_callback, init, init_asset_upload, init_proposal, - init_proposal_asset_upload, init_proposal_many_assets_upload, list_assets, - list_controllers, list_custom_domains, list_docs, list_proposals, list_rules, - post_upgrade, pre_upgrade, reject_proposal, set_asset_token, set_auth_config, - set_controllers, set_custom_domain, set_db_config, set_doc, set_many_docs, set_rule, - set_storage_config, submit_proposal, switch_storage_system_memory, upload_asset_chunk, + deposit_cycles, get_asset, get_auth_config, get_automation_config, get_config, + get_db_config, get_delegation, get_doc, get_many_assets, get_many_docs, get_proposal, + get_storage_config, http_request, http_request_streaming_callback, init, + init_asset_upload, init_proposal, init_proposal_asset_upload, + init_proposal_many_assets_upload, list_assets, list_controllers, list_custom_domains, + list_docs, list_proposals, list_rules, post_upgrade, pre_upgrade, reject_proposal, + set_asset_token, set_auth_config, set_automation_config, set_controllers, + set_custom_domain, set_db_config, set_doc, set_many_docs, set_rule, set_storage_config, + submit_proposal, switch_storage_system_memory, upload_asset_chunk, upload_proposal_asset_chunk, }; diff --git a/src/libs/satellite/src/memory/lifecycle.rs b/src/libs/satellite/src/memory/lifecycle.rs index 58d132baa3..a1471bc16c 100644 --- a/src/libs/satellite/src/memory/lifecycle.rs +++ b/src/libs/satellite/src/memory/lifecycle.rs @@ -6,6 +6,7 @@ use crate::memory::internal::{get_memory_for_upgrade, init_stable_state}; use crate::memory::state::STATE; use crate::memory::utils::init_storage_heap_state; use crate::random::init::defer_init_random_seed; +use crate::rules::upgrade::init_automation_collections; use crate::types::state::{HeapState, RuntimeState, State}; use ciborium::{from_reader, into_writer}; use junobuild_shared::memory::upgrade::{read_post_upgrade, write_pre_upgrade}; @@ -61,4 +62,7 @@ pub fn post_upgrade() { invoke_on_post_upgrade_sync(); invoke_on_post_upgrade(); + + // TODO: to be removed - one time upgrade! + init_automation_collections(); } diff --git a/src/libs/satellite/src/rules/mod.rs b/src/libs/satellite/src/rules/mod.rs index 19e827c9ab..4fdf93e1bd 100644 --- a/src/libs/satellite/src/rules/mod.rs +++ b/src/libs/satellite/src/rules/mod.rs @@ -1,3 +1,4 @@ mod internal; pub mod store; pub mod switch_memory; +pub mod upgrade; diff --git a/src/libs/satellite/src/rules/upgrade.rs b/src/libs/satellite/src/rules/upgrade.rs new file mode 100644 index 0000000000..5a00a07bd5 --- /dev/null +++ b/src/libs/satellite/src/rules/upgrade.rs @@ -0,0 +1,80 @@ +use crate::memory::state::STATE; +use ic_cdk::api::time; +use junobuild_collections::constants::db::{ + COLLECTION_AUTOMATION_TOKEN_DEFAULT_RULE, COLLECTION_AUTOMATION_TOKEN_KEY, + COLLECTION_AUTOMATION_WORKFLOW_DEFAULT_RULE, COLLECTION_AUTOMATION_WORKFLOW_KEY, +}; +use junobuild_collections::types::rules::Rule; + +// --------------------------------------------------------- +// One time upgrade +// --------------------------------------------------------- + +pub fn init_automation_collections() { + init_automation_token_collection(); + init_automation_workflow_collection(); +} + +fn init_automation_token_collection() { + let col = STATE.with(|state| { + let rules = &state.borrow_mut().heap.db.rules; + rules.get(COLLECTION_AUTOMATION_TOKEN_KEY).cloned() + }); + + if col.is_none() { + STATE.with(|state| { + let rules = &mut state.borrow_mut().heap.db.rules; + + let now = time(); + + let rule = Rule { + read: COLLECTION_AUTOMATION_TOKEN_DEFAULT_RULE.read, + write: COLLECTION_AUTOMATION_TOKEN_DEFAULT_RULE.write, + memory: COLLECTION_AUTOMATION_TOKEN_DEFAULT_RULE.memory, + mutable_permissions: COLLECTION_AUTOMATION_TOKEN_DEFAULT_RULE.mutable_permissions, + max_size: COLLECTION_AUTOMATION_TOKEN_DEFAULT_RULE.max_size, + max_capacity: COLLECTION_AUTOMATION_TOKEN_DEFAULT_RULE.max_capacity, + max_changes_per_user: COLLECTION_AUTOMATION_TOKEN_DEFAULT_RULE.max_changes_per_user, + created_at: now, + updated_at: now, + version: COLLECTION_AUTOMATION_TOKEN_DEFAULT_RULE.version, + rate_config: COLLECTION_AUTOMATION_TOKEN_DEFAULT_RULE.rate_config, + }; + + rules.insert(COLLECTION_AUTOMATION_TOKEN_KEY.to_string(), rule.clone()); + }); + } +} + +fn init_automation_workflow_collection() { + let col = STATE.with(|state| { + let rules = &state.borrow_mut().heap.db.rules; + rules.get(COLLECTION_AUTOMATION_WORKFLOW_KEY).cloned() + }); + + if col.is_none() { + STATE.with(|state| { + let rules = &mut state.borrow_mut().heap.db.rules; + + let now = time(); + + let rule = Rule { + read: COLLECTION_AUTOMATION_WORKFLOW_DEFAULT_RULE.read, + write: COLLECTION_AUTOMATION_WORKFLOW_DEFAULT_RULE.write, + memory: COLLECTION_AUTOMATION_WORKFLOW_DEFAULT_RULE.memory, + mutable_permissions: COLLECTION_AUTOMATION_WORKFLOW_DEFAULT_RULE + .mutable_permissions, + max_size: COLLECTION_AUTOMATION_WORKFLOW_DEFAULT_RULE.max_size, + max_capacity: COLLECTION_AUTOMATION_WORKFLOW_DEFAULT_RULE.max_capacity, + max_changes_per_user: COLLECTION_AUTOMATION_WORKFLOW_DEFAULT_RULE + .max_changes_per_user, + created_at: now, + updated_at: now, + version: COLLECTION_AUTOMATION_WORKFLOW_DEFAULT_RULE.version, + rate_config: COLLECTION_AUTOMATION_WORKFLOW_DEFAULT_RULE.rate_config, + }; + + rules.insert(COLLECTION_AUTOMATION_WORKFLOW_KEY.to_string(), rule.clone()); + }); + } +} diff --git a/src/libs/satellite/src/types.rs b/src/libs/satellite/src/types.rs index cfcc94b79e..5b180513d0 100644 --- a/src/libs/satellite/src/types.rs +++ b/src/libs/satellite/src/types.rs @@ -56,13 +56,16 @@ pub mod state { } pub mod interface { + use crate::automation::types::AuthenticationAutomationError; use crate::db::types::config::DbConfig; use crate::Doc; use candid::CandidType; + use junobuild_auth::automation::types::PreparedAutomation; use junobuild_auth::delegation::types::{ GetDelegationError, OpenIdGetDelegationArgs, OpenIdPrepareDelegationArgs, PrepareDelegationError, PreparedDelegation, SignedDelegation, }; + use junobuild_auth::state::types::automation::AutomationConfig; use junobuild_auth::state::types::config::AuthenticationConfig; use junobuild_cdn::proposals::ProposalId; use junobuild_storage::types::config::StorageConfig; @@ -73,6 +76,7 @@ pub mod interface { pub storage: StorageConfig, pub db: Option, pub authentication: Option, + pub automation: Option, } #[derive(CandidType, Serialize, Deserialize, Clone)] @@ -118,6 +122,12 @@ pub mod interface { Ok(SignedDelegation), Err(GetDelegationError), } + + #[derive(CandidType, Serialize, Deserialize)] + pub enum AuthenticateAutomationResultResponse { + Ok(PreparedAutomation), + Err(AuthenticationAutomationError), + } } pub mod store { diff --git a/src/satellite/satellite.did b/src/satellite/satellite.did index a7b4c29b57..af75dc32f8 100644 --- a/src/satellite/satellite.did +++ b/src/satellite/satellite.did @@ -22,12 +22,25 @@ type AssetNoContent = record { version : opt nat64; }; type AssetsUpgradeOptions = record { clear_existing_assets : opt bool }; +type AuthenticateAutomationArgs = variant { + OpenId : OpenIdPrepareAutomationArgs; +}; +type AuthenticateAutomationResultResponse = variant { + Ok : record { principal; AutomationController }; + Err : AuthenticationAutomationError; +}; type AuthenticateResultResponse = variant { Ok : Authentication; Err : AuthenticationError; }; type Authentication = record { doc : Doc; delegation : PreparedDelegation }; type AuthenticationArgs = variant { OpenId : OpenIdPrepareDelegationArgs }; +type AuthenticationAutomationError = variant { + PrepareAutomation : PrepareAutomationError; + RegisterController : text; + SaveWorkflowMetadata : text; + SaveUniqueJtiToken : text; +}; type AuthenticationConfig = record { updated_at : opt nat64; openid : opt AuthenticationConfigOpenId; @@ -49,6 +62,24 @@ type AuthenticationError = variant { RegisterUser : text; }; type AuthenticationRules = record { allowed_callers : vec principal }; +type AutomationConfig = record { + updated_at : opt nat64; + openid : opt AutomationConfigOpenId; + created_at : opt nat64; + version : opt nat64; +}; +type AutomationConfigOpenId = record { + observatory_id : opt principal; + providers : vec record { + OpenIdAutomationProvider; + OpenIdAutomationProviderConfig; + }; +}; +type AutomationController = record { + scope : AutomationScope; + expires_at : nat64; +}; +type AutomationScope = variant { Write; Submit }; type CollectionType = variant { Db; Storage }; type CommitBatch = record { batch_id : nat; @@ -60,6 +91,7 @@ type Config = record { db : opt DbConfig; authentication : opt AuthenticationConfig; storage : StorageConfig; + automation : opt AutomationConfig; }; type ConfigMaxMemorySize = record { stable : opt nat64; heap : opt nat64 }; type Controller = record { @@ -222,6 +254,16 @@ type OpenIdAuthProviderDelegationConfig = record { targets : opt vec principal; max_time_to_live : opt nat64; }; +type OpenIdAutomationProvider = variant { GitHub }; +type OpenIdAutomationProviderConfig = record { + controller : opt OpenIdAutomationProviderControllerConfig; + repositories : vec record { RepositoryKey; OpenIdAutomationRepositoryConfig }; +}; +type OpenIdAutomationProviderControllerConfig = record { + scope : opt AutomationScope; + max_time_to_live : opt nat64; +}; +type OpenIdAutomationRepositoryConfig = record { branches : opt vec text }; type OpenIdDelegationProvider = variant { GitHub; Google }; type OpenIdGetDelegationArgs = record { jwt : text; @@ -229,12 +271,22 @@ type OpenIdGetDelegationArgs = record { salt : blob; expiration : nat64; }; +type OpenIdPrepareAutomationArgs = record { jwt : text; salt : blob }; type OpenIdPrepareDelegationArgs = record { jwt : text; session_key : blob; salt : blob; }; type Permission = variant { Controllers; Private; Public; Managed }; +type PrepareAutomationError = variant { + JwtFindProvider : JwtFindProviderError; + InvalidController : text; + GetCachedJwks; + JwtVerify : JwtVerifyError; + GetOrFetchJwks : GetOrRefreshJwksError; + ControllerAlreadyExists; + TooManyControllers : text; +}; type PrepareDelegationError = variant { JwtFindProvider : JwtFindProviderError; GetCachedJwks; @@ -267,6 +319,7 @@ type ProposalType = variant { SegmentsDeployment : SegmentsDeploymentOptions; }; type RateConfig = record { max_tokens : nat64; time_per_token_ns : nat64 }; +type RepositoryKey = record { owner : text; name : text }; type Rule = record { max_capacity : opt nat32; memory : opt Memory; @@ -291,6 +344,10 @@ type SetAuthenticationConfig = record { internet_identity : opt AuthenticationConfigInternetIdentity; rules : opt AuthenticationRules; }; +type SetAutomationConfig = record { + openid : opt AutomationConfigOpenId; + version : opt nat64; +}; type SetController = record { metadata : vec record { text; text }; kind : opt ControllerKind; @@ -378,6 +435,9 @@ type UploadChunk = record { type UploadChunkResult = record { chunk_id : nat }; service : (InitSatelliteArgs) -> { authenticate : (AuthenticationArgs) -> (AuthenticateResultResponse); + authenticate_automation : (AuthenticateAutomationArgs) -> ( + AuthenticateAutomationResultResponse, + ); commit_asset_upload : (CommitBatch) -> (); commit_proposal : (CommitProposal) -> (null); commit_proposal_asset_upload : (CommitBatch) -> (); @@ -404,6 +464,7 @@ service : (InitSatelliteArgs) -> { deposit_cycles : (DepositCyclesArgs) -> (); get_asset : (text, text) -> (opt AssetNoContent) query; get_auth_config : () -> (opt AuthenticationConfig) query; + get_automation_config : () -> (opt AutomationConfig) query; get_config : () -> (Config); get_db_config : () -> (opt DbConfig) query; get_delegation : (GetDelegationArgs) -> (GetDelegationResultResponse) query; @@ -437,6 +498,7 @@ service : (InitSatelliteArgs) -> { reject_proposal : (CommitProposal) -> (null); set_asset_token : (text, text, opt text) -> (); set_auth_config : (SetAuthenticationConfig) -> (AuthenticationConfig); + set_automation_config : (SetAutomationConfig) -> (AutomationConfig); set_controllers : (SetControllersArgs) -> ( vec record { principal; Controller }, ); diff --git a/src/sputnik/sputnik.did b/src/sputnik/sputnik.did index 26f6a05509..84b9e9666f 100644 --- a/src/sputnik/sputnik.did +++ b/src/sputnik/sputnik.did @@ -22,12 +22,25 @@ type AssetNoContent = record { version : opt nat64; }; type AssetsUpgradeOptions = record { clear_existing_assets : opt bool }; +type AuthenticateAutomationArgs = variant { + OpenId : OpenIdPrepareAutomationArgs; +}; +type AuthenticateAutomationResultResponse = variant { + Ok : record { principal; AutomationController }; + Err : AuthenticationAutomationError; +}; type AuthenticateResultResponse = variant { Ok : Authentication; Err : AuthenticationError; }; type Authentication = record { doc : Doc; delegation : PreparedDelegation }; type AuthenticationArgs = variant { OpenId : OpenIdPrepareDelegationArgs }; +type AuthenticationAutomationError = variant { + PrepareAutomation : PrepareAutomationError; + RegisterController : text; + SaveWorkflowMetadata : text; + SaveUniqueJtiToken : text; +}; type AuthenticationConfig = record { updated_at : opt nat64; openid : opt AuthenticationConfigOpenId; @@ -49,6 +62,24 @@ type AuthenticationError = variant { RegisterUser : text; }; type AuthenticationRules = record { allowed_callers : vec principal }; +type AutomationConfig = record { + updated_at : opt nat64; + openid : opt AutomationConfigOpenId; + created_at : opt nat64; + version : opt nat64; +}; +type AutomationConfigOpenId = record { + observatory_id : opt principal; + providers : vec record { + OpenIdAutomationProvider; + OpenIdAutomationProviderConfig; + }; +}; +type AutomationController = record { + scope : AutomationScope; + expires_at : nat64; +}; +type AutomationScope = variant { Write; Submit }; type CollectionType = variant { Db; Storage }; type CommitBatch = record { batch_id : nat; @@ -60,6 +91,7 @@ type Config = record { db : opt DbConfig; authentication : opt AuthenticationConfig; storage : StorageConfig; + automation : opt AutomationConfig; }; type ConfigMaxMemorySize = record { stable : opt nat64; heap : opt nat64 }; type Controller = record { @@ -222,6 +254,16 @@ type OpenIdAuthProviderDelegationConfig = record { targets : opt vec principal; max_time_to_live : opt nat64; }; +type OpenIdAutomationProvider = variant { GitHub }; +type OpenIdAutomationProviderConfig = record { + controller : opt OpenIdAutomationProviderControllerConfig; + repositories : vec record { RepositoryKey; OpenIdAutomationRepositoryConfig }; +}; +type OpenIdAutomationProviderControllerConfig = record { + scope : opt AutomationScope; + max_time_to_live : opt nat64; +}; +type OpenIdAutomationRepositoryConfig = record { branches : opt vec text }; type OpenIdDelegationProvider = variant { GitHub; Google }; type OpenIdGetDelegationArgs = record { jwt : text; @@ -229,12 +271,22 @@ type OpenIdGetDelegationArgs = record { salt : blob; expiration : nat64; }; +type OpenIdPrepareAutomationArgs = record { jwt : text; salt : blob }; type OpenIdPrepareDelegationArgs = record { jwt : text; session_key : blob; salt : blob; }; type Permission = variant { Controllers; Private; Public; Managed }; +type PrepareAutomationError = variant { + JwtFindProvider : JwtFindProviderError; + InvalidController : text; + GetCachedJwks; + JwtVerify : JwtVerifyError; + GetOrFetchJwks : GetOrRefreshJwksError; + ControllerAlreadyExists; + TooManyControllers : text; +}; type PrepareDelegationError = variant { JwtFindProvider : JwtFindProviderError; GetCachedJwks; @@ -267,6 +319,7 @@ type ProposalType = variant { SegmentsDeployment : SegmentsDeploymentOptions; }; type RateConfig = record { max_tokens : nat64; time_per_token_ns : nat64 }; +type RepositoryKey = record { owner : text; name : text }; type Rule = record { max_capacity : opt nat32; memory : opt Memory; @@ -291,6 +344,10 @@ type SetAuthenticationConfig = record { internet_identity : opt AuthenticationConfigInternetIdentity; rules : opt AuthenticationRules; }; +type SetAutomationConfig = record { + openid : opt AutomationConfigOpenId; + version : opt nat64; +}; type SetController = record { metadata : vec record { text; text }; kind : opt ControllerKind; @@ -378,6 +435,9 @@ type UploadChunk = record { type UploadChunkResult = record { chunk_id : nat }; service : (InitSatelliteArgs) -> { authenticate : (AuthenticationArgs) -> (AuthenticateResultResponse); + authenticate_automation : (AuthenticateAutomationArgs) -> ( + AuthenticateAutomationResultResponse, + ); commit_asset_upload : (CommitBatch) -> (); commit_proposal : (CommitProposal) -> (null); commit_proposal_asset_upload : (CommitBatch) -> (); @@ -404,6 +464,7 @@ service : (InitSatelliteArgs) -> { deposit_cycles : (DepositCyclesArgs) -> (); get_asset : (text, text) -> (opt AssetNoContent) query; get_auth_config : () -> (opt AuthenticationConfig) query; + get_automation_config : () -> (opt AutomationConfig) query; get_config : () -> (Config); get_db_config : () -> (opt DbConfig) query; get_delegation : (GetDelegationArgs) -> (GetDelegationResultResponse) query; @@ -437,6 +498,7 @@ service : (InitSatelliteArgs) -> { reject_proposal : (CommitProposal) -> (null); set_asset_token : (text, text, opt text) -> (); set_auth_config : (SetAuthenticationConfig) -> (AuthenticationConfig); + set_automation_config : (SetAutomationConfig) -> (AutomationConfig); set_controllers : (SetControllersArgs) -> ( vec record { principal; Controller }, ); 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 5f232ebcba..e8d40d0cbe 100644 --- a/src/tests/declarations/test_satellite/test_satellite.did.d.ts +++ b/src/tests/declarations/test_satellite/test_satellite.did.d.ts @@ -34,12 +34,27 @@ export interface AssetNoContent { export interface AssetsUpgradeOptions { clear_existing_assets: [] | [boolean]; } +export type AuthenticateAutomationArgs = { + OpenId: OpenIdPrepareAutomationArgs; +}; +export type AuthenticateAutomationResultResponse = + | { + Ok: [Principal, AutomationController]; + } + | { Err: AuthenticationAutomationError }; export type AuthenticateResultResponse = { Ok: Authentication } | { Err: AuthenticationError }; export interface Authentication { doc: Doc; delegation: PreparedDelegation; } export type AuthenticationArgs = { OpenId: OpenIdPrepareDelegationArgs }; +export type AuthenticationAutomationError = + | { + PrepareAutomation: PrepareAutomationError; + } + | { RegisterController: string } + | { SaveWorkflowMetadata: string } + | { SaveUniqueJtiToken: string }; export interface AuthenticationConfig { updated_at: [] | [bigint]; openid: [] | [AuthenticationConfigOpenId]; @@ -64,6 +79,21 @@ export type AuthenticationError = export interface AuthenticationRules { allowed_callers: Array; } +export interface AutomationConfig { + updated_at: [] | [bigint]; + openid: [] | [AutomationConfigOpenId]; + created_at: [] | [bigint]; + version: [] | [bigint]; +} +export interface AutomationConfigOpenId { + observatory_id: [] | [Principal]; + providers: Array<[OpenIdAutomationProvider, OpenIdAutomationProviderConfig]>; +} +export interface AutomationController { + scope: AutomationScope; + expires_at: bigint; +} +export type AutomationScope = { Write: null } | { Submit: null }; export type CollectionType = { Db: null } | { Storage: null }; export interface CommitBatch { batch_id: bigint; @@ -78,6 +108,7 @@ export interface Config { db: [] | [DbConfig]; authentication: [] | [AuthenticationConfig]; storage: StorageConfig; + automation: [] | [AutomationConfig]; } export interface ConfigMaxMemorySize { stable: [] | [bigint]; @@ -269,6 +300,18 @@ export interface OpenIdAuthProviderDelegationConfig { targets: [] | [Array]; max_time_to_live: [] | [bigint]; } +export type OpenIdAutomationProvider = { GitHub: null }; +export interface OpenIdAutomationProviderConfig { + controller: [] | [OpenIdAutomationProviderControllerConfig]; + repositories: Array<[RepositoryKey, OpenIdAutomationRepositoryConfig]>; +} +export interface OpenIdAutomationProviderControllerConfig { + scope: [] | [AutomationScope]; + max_time_to_live: [] | [bigint]; +} +export interface OpenIdAutomationRepositoryConfig { + branches: [] | [Array]; +} export type OpenIdDelegationProvider = { GitHub: null } | { Google: null }; export interface OpenIdGetDelegationArgs { jwt: string; @@ -276,6 +319,10 @@ export interface OpenIdGetDelegationArgs { salt: Uint8Array; expiration: bigint; } +export interface OpenIdPrepareAutomationArgs { + jwt: string; + salt: Uint8Array; +} export interface OpenIdPrepareDelegationArgs { jwt: string; session_key: Uint8Array; @@ -286,6 +333,16 @@ export type Permission = | { Private: null } | { Public: null } | { Managed: null }; +export type PrepareAutomationError = + | { + JwtFindProvider: JwtFindProviderError; + } + | { InvalidController: string } + | { GetCachedJwks: null } + | { JwtVerify: JwtVerifyError } + | { GetOrFetchJwks: GetOrRefreshJwksError } + | { ControllerAlreadyExists: null } + | { TooManyControllers: string }; export type PrepareDelegationError = | { JwtFindProvider: JwtFindProviderError; @@ -325,6 +382,10 @@ export interface RateConfig { max_tokens: bigint; time_per_token_ns: bigint; } +export interface RepositoryKey { + owner: string; + name: string; +} export type Result = { Ok: number } | { Err: string }; export interface Rule { max_capacity: [] | [number]; @@ -350,6 +411,10 @@ export interface SetAuthenticationConfig { internet_identity: [] | [AuthenticationConfigInternetIdentity]; rules: [] | [AuthenticationRules]; } +export interface SetAutomationConfig { + openid: [] | [AutomationConfigOpenId]; + version: [] | [bigint]; +} export interface SetController { metadata: Array<[string, string]>; kind: [] | [ControllerKind]; @@ -444,6 +509,10 @@ export interface UploadChunkResult { } export interface _SERVICE { authenticate: ActorMethod<[AuthenticationArgs], AuthenticateResultResponse>; + authenticate_automation: ActorMethod< + [AuthenticateAutomationArgs], + AuthenticateAutomationResultResponse + >; commit_asset_upload: ActorMethod<[CommitBatch], undefined>; commit_proposal: ActorMethod<[CommitProposal], null>; commit_proposal_asset_upload: ActorMethod<[CommitBatch], undefined>; @@ -468,6 +537,7 @@ export interface _SERVICE { deposit_cycles: ActorMethod<[DepositCyclesArgs], undefined>; get_asset: ActorMethod<[string, string], [] | [AssetNoContent]>; get_auth_config: ActorMethod<[], [] | [AuthenticationConfig]>; + get_automation_config: ActorMethod<[], [] | [AutomationConfig]>; get_config: ActorMethod<[], Config>; get_db_config: ActorMethod<[], [] | [DbConfig]>; get_delegation: ActorMethod<[GetDelegationArgs], GetDelegationResultResponse>; @@ -499,6 +569,7 @@ export interface _SERVICE { reject_proposal: ActorMethod<[CommitProposal], null>; set_asset_token: ActorMethod<[string, string, [] | [string]], undefined>; set_auth_config: ActorMethod<[SetAuthenticationConfig], AuthenticationConfig>; + set_automation_config: ActorMethod<[SetAutomationConfig], AutomationConfig>; set_controllers: ActorMethod<[SetControllersArgs], Array<[Principal, Controller]>>; set_custom_domain: ActorMethod<[string, [] | [string]], undefined>; set_db_config: ActorMethod<[SetDbConfig], DbConfig>; 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 bf6976362b..6fc6385bbf 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,40 @@ export const idlFactory = ({ IDL }) => { Ok: Authentication, Err: AuthenticationError }); + const OpenIdPrepareAutomationArgs = IDL.Record({ + jwt: IDL.Text, + salt: IDL.Vec(IDL.Nat8) + }); + const AuthenticateAutomationArgs = IDL.Variant({ + OpenId: OpenIdPrepareAutomationArgs + }); + const AutomationScope = IDL.Variant({ + Write: IDL.Null, + Submit: IDL.Null + }); + const AutomationController = IDL.Record({ + scope: AutomationScope, + expires_at: IDL.Nat64 + }); + const PrepareAutomationError = IDL.Variant({ + JwtFindProvider: JwtFindProviderError, + InvalidController: IDL.Text, + GetCachedJwks: IDL.Null, + JwtVerify: JwtVerifyError, + GetOrFetchJwks: GetOrRefreshJwksError, + ControllerAlreadyExists: IDL.Null, + TooManyControllers: IDL.Text + }); + const AuthenticationAutomationError = IDL.Variant({ + PrepareAutomation: PrepareAutomationError, + RegisterController: IDL.Text, + SaveWorkflowMetadata: IDL.Text, + SaveUniqueJtiToken: IDL.Text + }); + const AuthenticateAutomationResultResponse = IDL.Variant({ + Ok: IDL.Tuple(IDL.Principal, AutomationController), + Err: AuthenticationAutomationError + }); const CommitBatch = IDL.Record({ batch_id: IDL.Nat, headers: IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text)), @@ -194,6 +228,29 @@ export const idlFactory = ({ IDL }) => { internet_identity: IDL.Opt(AuthenticationConfigInternetIdentity), rules: IDL.Opt(AuthenticationRules) }); + const OpenIdAutomationProvider = IDL.Variant({ GitHub: IDL.Null }); + const OpenIdAutomationProviderControllerConfig = IDL.Record({ + scope: IDL.Opt(AutomationScope), + max_time_to_live: IDL.Opt(IDL.Nat64) + }); + const RepositoryKey = IDL.Record({ owner: IDL.Text, name: IDL.Text }); + const OpenIdAutomationRepositoryConfig = IDL.Record({ + branches: IDL.Opt(IDL.Vec(IDL.Text)) + }); + const OpenIdAutomationProviderConfig = IDL.Record({ + controller: IDL.Opt(OpenIdAutomationProviderControllerConfig), + repositories: IDL.Vec(IDL.Tuple(RepositoryKey, OpenIdAutomationRepositoryConfig)) + }); + const AutomationConfigOpenId = IDL.Record({ + observatory_id: IDL.Opt(IDL.Principal), + providers: IDL.Vec(IDL.Tuple(OpenIdAutomationProvider, OpenIdAutomationProviderConfig)) + }); + const AutomationConfig = IDL.Record({ + updated_at: IDL.Opt(IDL.Nat64), + openid: IDL.Opt(AutomationConfigOpenId), + created_at: IDL.Opt(IDL.Nat64), + version: IDL.Opt(IDL.Nat64) + }); const ConfigMaxMemorySize = IDL.Record({ stable: IDL.Opt(IDL.Nat64), heap: IDL.Opt(IDL.Nat64) @@ -231,7 +288,8 @@ export const idlFactory = ({ IDL }) => { const Config = IDL.Record({ db: IDL.Opt(DbConfig), authentication: IDL.Opt(AuthenticationConfig), - storage: StorageConfig + storage: StorageConfig, + automation: IDL.Opt(AutomationConfig) }); const OpenIdGetDelegationArgs = IDL.Record({ jwt: IDL.Text, @@ -404,6 +462,10 @@ export const idlFactory = ({ IDL }) => { internet_identity: IDL.Opt(AuthenticationConfigInternetIdentity), rules: IDL.Opt(AuthenticationRules) }); + const SetAutomationConfig = IDL.Record({ + openid: IDL.Opt(AutomationConfigOpenId), + version: IDL.Opt(IDL.Nat64) + }); const SetController = IDL.Record({ metadata: IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text)), kind: IDL.Opt(ControllerKind), @@ -453,6 +515,11 @@ export const idlFactory = ({ IDL }) => { return IDL.Service({ authenticate: IDL.Func([AuthenticationArgs], [AuthenticateResultResponse], []), + authenticate_automation: IDL.Func( + [AuthenticateAutomationArgs], + [AuthenticateAutomationResultResponse], + [] + ), commit_asset_upload: IDL.Func([CommitBatch], [], []), commit_proposal: IDL.Func([CommitProposal], [IDL.Null], []), commit_proposal_asset_upload: IDL.Func([CommitBatch], [], []), @@ -481,6 +548,7 @@ export const idlFactory = ({ IDL }) => { deposit_cycles: IDL.Func([DepositCyclesArgs], [], []), get_asset: IDL.Func([IDL.Text, IDL.Text], [IDL.Opt(AssetNoContent)], []), get_auth_config: IDL.Func([], [IDL.Opt(AuthenticationConfig)], []), + get_automation_config: IDL.Func([], [IDL.Opt(AutomationConfig)], []), get_config: IDL.Func([], [Config], []), get_db_config: IDL.Func([], [IDL.Opt(DbConfig)], []), get_delegation: IDL.Func([GetDelegationArgs], [GetDelegationResultResponse], []), @@ -522,6 +590,7 @@ export const idlFactory = ({ IDL }) => { reject_proposal: IDL.Func([CommitProposal], [IDL.Null], []), set_asset_token: IDL.Func([IDL.Text, IDL.Text, IDL.Opt(IDL.Text)], [], []), set_auth_config: IDL.Func([SetAuthenticationConfig], [AuthenticationConfig], []), + set_automation_config: IDL.Func([SetAutomationConfig], [AutomationConfig], []), set_controllers: IDL.Func( [SetControllersArgs], [IDL.Vec(IDL.Tuple(IDL.Principal, Controller))], 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 794fe291c0..f718438304 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,40 @@ export const idlFactory = ({ IDL }) => { Ok: Authentication, Err: AuthenticationError }); + const OpenIdPrepareAutomationArgs = IDL.Record({ + jwt: IDL.Text, + salt: IDL.Vec(IDL.Nat8) + }); + const AuthenticateAutomationArgs = IDL.Variant({ + OpenId: OpenIdPrepareAutomationArgs + }); + const AutomationScope = IDL.Variant({ + Write: IDL.Null, + Submit: IDL.Null + }); + const AutomationController = IDL.Record({ + scope: AutomationScope, + expires_at: IDL.Nat64 + }); + const PrepareAutomationError = IDL.Variant({ + JwtFindProvider: JwtFindProviderError, + InvalidController: IDL.Text, + GetCachedJwks: IDL.Null, + JwtVerify: JwtVerifyError, + GetOrFetchJwks: GetOrRefreshJwksError, + ControllerAlreadyExists: IDL.Null, + TooManyControllers: IDL.Text + }); + const AuthenticationAutomationError = IDL.Variant({ + PrepareAutomation: PrepareAutomationError, + RegisterController: IDL.Text, + SaveWorkflowMetadata: IDL.Text, + SaveUniqueJtiToken: IDL.Text + }); + const AuthenticateAutomationResultResponse = IDL.Variant({ + Ok: IDL.Tuple(IDL.Principal, AutomationController), + Err: AuthenticationAutomationError + }); const CommitBatch = IDL.Record({ batch_id: IDL.Nat, headers: IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text)), @@ -194,6 +228,29 @@ export const idlFactory = ({ IDL }) => { internet_identity: IDL.Opt(AuthenticationConfigInternetIdentity), rules: IDL.Opt(AuthenticationRules) }); + const OpenIdAutomationProvider = IDL.Variant({ GitHub: IDL.Null }); + const OpenIdAutomationProviderControllerConfig = IDL.Record({ + scope: IDL.Opt(AutomationScope), + max_time_to_live: IDL.Opt(IDL.Nat64) + }); + const RepositoryKey = IDL.Record({ owner: IDL.Text, name: IDL.Text }); + const OpenIdAutomationRepositoryConfig = IDL.Record({ + branches: IDL.Opt(IDL.Vec(IDL.Text)) + }); + const OpenIdAutomationProviderConfig = IDL.Record({ + controller: IDL.Opt(OpenIdAutomationProviderControllerConfig), + repositories: IDL.Vec(IDL.Tuple(RepositoryKey, OpenIdAutomationRepositoryConfig)) + }); + const AutomationConfigOpenId = IDL.Record({ + observatory_id: IDL.Opt(IDL.Principal), + providers: IDL.Vec(IDL.Tuple(OpenIdAutomationProvider, OpenIdAutomationProviderConfig)) + }); + const AutomationConfig = IDL.Record({ + updated_at: IDL.Opt(IDL.Nat64), + openid: IDL.Opt(AutomationConfigOpenId), + created_at: IDL.Opt(IDL.Nat64), + version: IDL.Opt(IDL.Nat64) + }); const ConfigMaxMemorySize = IDL.Record({ stable: IDL.Opt(IDL.Nat64), heap: IDL.Opt(IDL.Nat64) @@ -231,7 +288,8 @@ export const idlFactory = ({ IDL }) => { const Config = IDL.Record({ db: IDL.Opt(DbConfig), authentication: IDL.Opt(AuthenticationConfig), - storage: StorageConfig + storage: StorageConfig, + automation: IDL.Opt(AutomationConfig) }); const OpenIdGetDelegationArgs = IDL.Record({ jwt: IDL.Text, @@ -404,6 +462,10 @@ export const idlFactory = ({ IDL }) => { internet_identity: IDL.Opt(AuthenticationConfigInternetIdentity), rules: IDL.Opt(AuthenticationRules) }); + const SetAutomationConfig = IDL.Record({ + openid: IDL.Opt(AutomationConfigOpenId), + version: IDL.Opt(IDL.Nat64) + }); const SetController = IDL.Record({ metadata: IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text)), kind: IDL.Opt(ControllerKind), @@ -453,6 +515,11 @@ export const idlFactory = ({ IDL }) => { return IDL.Service({ authenticate: IDL.Func([AuthenticationArgs], [AuthenticateResultResponse], []), + authenticate_automation: IDL.Func( + [AuthenticateAutomationArgs], + [AuthenticateAutomationResultResponse], + [] + ), commit_asset_upload: IDL.Func([CommitBatch], [], []), commit_proposal: IDL.Func([CommitProposal], [IDL.Null], []), commit_proposal_asset_upload: IDL.Func([CommitBatch], [], []), @@ -481,6 +548,7 @@ export const idlFactory = ({ IDL }) => { deposit_cycles: IDL.Func([DepositCyclesArgs], [], []), get_asset: IDL.Func([IDL.Text, IDL.Text], [IDL.Opt(AssetNoContent)], ['query']), get_auth_config: IDL.Func([], [IDL.Opt(AuthenticationConfig)], ['query']), + get_automation_config: IDL.Func([], [IDL.Opt(AutomationConfig)], ['query']), get_config: IDL.Func([], [Config], []), get_db_config: IDL.Func([], [IDL.Opt(DbConfig)], ['query']), get_delegation: IDL.Func([GetDelegationArgs], [GetDelegationResultResponse], ['query']), @@ -522,6 +590,7 @@ export const idlFactory = ({ IDL }) => { reject_proposal: IDL.Func([CommitProposal], [IDL.Null], []), set_asset_token: IDL.Func([IDL.Text, IDL.Text, IDL.Opt(IDL.Text)], [], []), set_auth_config: IDL.Func([SetAuthenticationConfig], [AuthenticationConfig], []), + set_automation_config: IDL.Func([SetAutomationConfig], [AutomationConfig], []), set_controllers: IDL.Func( [SetControllersArgs], [IDL.Vec(IDL.Tuple(IDL.Principal, Controller))], diff --git a/src/tests/fixtures/test_satellite/test_satellite.did b/src/tests/fixtures/test_satellite/test_satellite.did index 1c04d35bdf..a9628a6e9b 100644 --- a/src/tests/fixtures/test_satellite/test_satellite.did +++ b/src/tests/fixtures/test_satellite/test_satellite.did @@ -22,12 +22,25 @@ type AssetNoContent = record { version : opt nat64; }; type AssetsUpgradeOptions = record { clear_existing_assets : opt bool }; +type AuthenticateAutomationArgs = variant { + OpenId : OpenIdPrepareAutomationArgs; +}; +type AuthenticateAutomationResultResponse = variant { + Ok : record { principal; AutomationController }; + Err : AuthenticationAutomationError; +}; type AuthenticateResultResponse = variant { Ok : Authentication; Err : AuthenticationError; }; type Authentication = record { doc : Doc; delegation : PreparedDelegation }; type AuthenticationArgs = variant { OpenId : OpenIdPrepareDelegationArgs }; +type AuthenticationAutomationError = variant { + PrepareAutomation : PrepareAutomationError; + RegisterController : text; + SaveWorkflowMetadata : text; + SaveUniqueJtiToken : text; +}; type AuthenticationConfig = record { updated_at : opt nat64; openid : opt AuthenticationConfigOpenId; @@ -49,6 +62,24 @@ type AuthenticationError = variant { RegisterUser : text; }; type AuthenticationRules = record { allowed_callers : vec principal }; +type AutomationConfig = record { + updated_at : opt nat64; + openid : opt AutomationConfigOpenId; + created_at : opt nat64; + version : opt nat64; +}; +type AutomationConfigOpenId = record { + observatory_id : opt principal; + providers : vec record { + OpenIdAutomationProvider; + OpenIdAutomationProviderConfig; + }; +}; +type AutomationController = record { + scope : AutomationScope; + expires_at : nat64; +}; +type AutomationScope = variant { Write; Submit }; type CollectionType = variant { Db; Storage }; type CommitBatch = record { batch_id : nat; @@ -60,6 +91,7 @@ type Config = record { db : opt DbConfig; authentication : opt AuthenticationConfig; storage : StorageConfig; + automation : opt AutomationConfig; }; type ConfigMaxMemorySize = record { stable : opt nat64; heap : opt nat64 }; type Controller = record { @@ -222,6 +254,16 @@ type OpenIdAuthProviderDelegationConfig = record { targets : opt vec principal; max_time_to_live : opt nat64; }; +type OpenIdAutomationProvider = variant { GitHub }; +type OpenIdAutomationProviderConfig = record { + controller : opt OpenIdAutomationProviderControllerConfig; + repositories : vec record { RepositoryKey; OpenIdAutomationRepositoryConfig }; +}; +type OpenIdAutomationProviderControllerConfig = record { + scope : opt AutomationScope; + max_time_to_live : opt nat64; +}; +type OpenIdAutomationRepositoryConfig = record { branches : opt vec text }; type OpenIdDelegationProvider = variant { GitHub; Google }; type OpenIdGetDelegationArgs = record { jwt : text; @@ -229,12 +271,22 @@ type OpenIdGetDelegationArgs = record { salt : blob; expiration : nat64; }; +type OpenIdPrepareAutomationArgs = record { jwt : text; salt : blob }; type OpenIdPrepareDelegationArgs = record { jwt : text; session_key : blob; salt : blob; }; type Permission = variant { Controllers; Private; Public; Managed }; +type PrepareAutomationError = variant { + JwtFindProvider : JwtFindProviderError; + InvalidController : text; + GetCachedJwks; + JwtVerify : JwtVerifyError; + GetOrFetchJwks : GetOrRefreshJwksError; + ControllerAlreadyExists; + TooManyControllers : text; +}; type PrepareDelegationError = variant { JwtFindProvider : JwtFindProviderError; GetCachedJwks; @@ -267,6 +319,7 @@ type ProposalType = variant { SegmentsDeployment : SegmentsDeploymentOptions; }; type RateConfig = record { max_tokens : nat64; time_per_token_ns : nat64 }; +type RepositoryKey = record { owner : text; name : text }; type Rule = record { max_capacity : opt nat32; memory : opt Memory; @@ -291,6 +344,10 @@ type SetAuthenticationConfig = record { internet_identity : opt AuthenticationConfigInternetIdentity; rules : opt AuthenticationRules; }; +type SetAutomationConfig = record { + openid : opt AutomationConfigOpenId; + version : opt nat64; +}; type SetController = record { metadata : vec record { text; text }; kind : opt ControllerKind; @@ -378,6 +435,9 @@ type UploadChunk = record { type UploadChunkResult = record { chunk_id : nat }; service : (InitSatelliteArgs) -> { authenticate : (AuthenticationArgs) -> (AuthenticateResultResponse); + authenticate_automation : (AuthenticateAutomationArgs) -> ( + AuthenticateAutomationResultResponse, + ); commit_asset_upload : (CommitBatch) -> (); commit_proposal : (CommitProposal) -> (null); commit_proposal_asset_upload : (CommitBatch) -> (); @@ -404,6 +464,7 @@ service : (InitSatelliteArgs) -> { deposit_cycles : (DepositCyclesArgs) -> (); get_asset : (text, text) -> (opt AssetNoContent) query; get_auth_config : () -> (opt AuthenticationConfig) query; + get_automation_config : () -> (opt AutomationConfig) query; get_config : () -> (Config); get_db_config : () -> (opt DbConfig) query; get_delegation : (GetDelegationArgs) -> (GetDelegationResultResponse) query; @@ -437,6 +498,7 @@ service : (InitSatelliteArgs) -> { reject_proposal : (CommitProposal) -> (null); set_asset_token : (text, text, opt text) -> (); set_auth_config : (SetAuthenticationConfig) -> (AuthenticationConfig); + set_automation_config : (SetAutomationConfig) -> (AutomationConfig); set_controllers : (SetControllersArgs) -> ( vec record { principal; Controller }, );