1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
use std::process;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;

use clap::Args;
use colorful::Colorful;
use miette::{miette, IntoDiagnostic, WrapErr};
use tokio::sync::Mutex;
use tokio::try_join;
use tracing::{info, warn};

use ockam::Context;
use ockam_api::cli_state::random_name;
use ockam_api::cloud::enroll::auth0::*;
use ockam_api::cloud::project::{Project, Projects};
use ockam_api::cloud::space::{Space, Spaces};
use ockam_api::cloud::Controller;
use ockam_api::enroll::enrollment::{EnrollStatus, Enrollment};
use ockam_api::enroll::oidc_service::OidcService;
use ockam_api::nodes::InMemoryNode;

use crate::enroll::OidcServiceExt;
use crate::node::util::initialize_default_node;
use crate::operation::util::check_for_project_completion;
use crate::output::OutputFormat;
use crate::project::util::check_project_readiness;
use crate::terminal::OckamColor;
use crate::util::node_rpc;
use crate::{display_parse_logs, docs, fmt_log, fmt_ok, fmt_para, CommandGlobalOpts, Result};

const LONG_ABOUT: &str = include_str!("./static/long_about.txt");
const AFTER_LONG_HELP: &str = include_str!("./static/after_long_help.txt");

/// Enroll with Ockam Orchestrator
#[derive(Clone, Debug, Args)]
#[command(
long_about = docs::about(LONG_ABOUT),
after_long_help = docs::after_help(AFTER_LONG_HELP)
)]
pub struct EnrollCommand {
    /// The name of an existing identity that you wish to enroll
    #[arg(global = true, value_name = "IDENTITY_NAME", long)]
    pub identity: Option<String>,

    /// Use PKCE authorization flow
    #[arg(long)]
    pub authorization_code_flow: bool,

    /// Skip creation of default Space and default Project
    #[arg(long)]
    pub user_account_only: bool,
}

impl EnrollCommand {
    pub fn run(self, opts: CommandGlobalOpts) {
        node_rpc(rpc, (opts, self));
    }
}

async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, EnrollCommand)) -> miette::Result<()> {
    if opts.global_args.output_format == OutputFormat::Json {
        return Err(miette::miette!(
            "The flag --output json is invalid for this command."
        ));
    }
    run_impl(&ctx, opts.clone(), cmd).await?;
    initialize_default_node(&ctx, &opts).await?;
    Ok(())
}

fn ctrlc_handler(opts: CommandGlobalOpts) {
    let is_confirmation = Arc::new(AtomicBool::new(false));
    ctrlc::set_handler(move || {
        if is_confirmation.load(Ordering::Relaxed) {
            let _ = opts.terminal.write_line(
                format!(
                    "\n{} Received Ctrl+C again. Cancelling {}. Please try again.",
                    "!".red(), "ockam enroll".bold().light_yellow()
                )
                    .as_str(),
            );
            process::exit(2);
        } else {
            let _ = opts.terminal.write_line(
                format!(
                    "\n{} {} is still in progress. If you would like to stop the enrollment process, press Ctrl+C again.",
                    "!".red(), "ockam enroll".bold().light_yellow()
                )
                    .as_str(),
            );
            is_confirmation.store(true, Ordering::Relaxed);
        }
    })
        .expect("Error setting Ctrl-C handler");
}

async fn run_impl(
    ctx: &Context,
    opts: CommandGlobalOpts,
    cmd: EnrollCommand,
) -> miette::Result<()> {
    opts.terminal.write_line(&fmt_log!(
        "Enrolling your default Ockam identity with Ockam Orchestrator...\n"
    ))?;

    ctrlc_handler(opts.clone());
    display_parse_logs(&opts);

    let oidc_service = OidcService::default();
    let token = if cmd.authorization_code_flow {
        oidc_service.get_token_with_pkce().await.into_diagnostic()?
    } else {
        oidc_service.get_token_interactively(&opts).await?
    };

    let user_info = oidc_service
        .wait_for_email_verification(&token, Some(&opts.terminal))
        .await?;
    opts.state.store_user(&user_info).await?;

    let identity_name = opts
        .state
        .get_named_identity_or_default(&cmd.identity)
        .await?
        .name();
    let node = InMemoryNode::start_node_with_identity(ctx, &opts.state, &identity_name).await?;
    let controller = node.create_controller().await?;

    enroll_with_node(&controller, ctx, token)
        .await
        .wrap_err("Failed to enroll your local identity with Ockam Orchestrator")?;
    let identifier = node.identifier();
    opts.state
        .set_identifier_as_enrolled(&identifier)
        .await
        .wrap_err("Unable to set the local identity as enrolled")?;
    info!("Enrolled a user with the Identifier {}", identifier);

    if let Err(e) = retrieve_user_project(&opts, ctx, &node, cmd.user_account_only).await {
        warn!(
            "Unable to retrieve your Orchestrator resources. Try running `ockam enroll` again or \
        create them manually using the `ockam space` and `ockam project` commands"
        );
        warn!("{e}");
    }

    opts.terminal.write_line(&fmt_ok!(
        "Enrolled {} as one of the Ockam identities of your Orchestrator account {}.",
        identifier
            .to_string()
            .color(OckamColor::PrimaryResource.color()),
        user_info.email
    ))?;
    Ok(())
}

async fn retrieve_user_project(
    opts: &CommandGlobalOpts,
    ctx: &Context,
    node: &InMemoryNode,
    user_account_only: bool,
) -> Result<Project> {
    // return the default project if there is one already stored locally
    if let Ok(project) = opts.state.get_default_project().await {
        return Ok(project);
    };

    let space = get_user_space(opts, ctx, node, user_account_only)
        .await
        .map_err(|e| {
            miette!(
                "Unable to retrieve and set a space as default {:?}",
                e.to_string()
            )
        })?
        .ok_or(miette!("No space was found"))?;

    info!("Retrieved the user default space {:?}", space);

    let project = get_user_project(opts, ctx, node, user_account_only, &space)
        .await
        .wrap_err(format!(
            "Unable to retrieve and set a project as default with space {}",
            space
                .name
                .to_string()
                .color(OckamColor::PrimaryResource.color())
        ))?
        .ok_or(miette!("No project was found"))?;
    info!("Retrieved the user default project {:?}", project);
    Ok(project)
}

/// Enroll a user with a token, using the controller
pub async fn enroll_with_node(
    controller: &Controller,
    ctx: &Context,
    token: OidcToken,
) -> miette::Result<()> {
    let reply = controller.enroll_with_oidc_token(ctx, token).await?;
    match reply {
        EnrollStatus::EnrolledSuccessfully => info!("Enrolled successfully"),
        EnrollStatus::AlreadyEnrolled => info!("Already enrolled"),
        EnrollStatus::UnexpectedStatus(e, s) => warn!("Unexpected status {s}. The error is: {e}"),
        EnrollStatus::FailedNoStatus(e) => warn!("A status was expected in the response to an enrollment request, got none. The error is: {e}"),
    };
    Ok(())
}

async fn get_user_space(
    opts: &CommandGlobalOpts,
    ctx: &Context,
    node: &InMemoryNode,
    user_account_only: bool,
) -> Result<Option<Space>> {
    // return the default space if there is one already stored locally
    if let Ok(space) = opts.state.get_default_space().await {
        return Ok(Some(space));
    };

    // Otherwise get the available spaces for node's identity
    // Those spaces might have been created previously and all the local state reset
    opts.terminal
        .write_line(&fmt_log!("Getting available spaces in your account..."))?;
    let is_finished = Mutex::new(false);
    let get_spaces = async {
        let spaces = node.get_spaces(ctx).await?;
        *is_finished.lock().await = true;
        Ok(spaces)
    };

    let message = vec![format!("Checking for any existing spaces...")];
    let progress_output = opts.terminal.progress_output(&message, &is_finished);

    let (spaces, _) = try_join!(get_spaces, progress_output)?;

    // If the identity has no spaces, create one
    let space = match spaces.first() {
        None => {
            if user_account_only {
                opts.terminal
                    .write_line(&fmt_para!("No spaces are defined in your account."))?;
                return Ok(None);
            }

            opts.terminal
                .write_line(&fmt_para!("No spaces are defined in your account."))?
                .write_line(&fmt_para!(
                    "Provisioning a space for you ({}) ...",
                    "if you don't use it for a few weeks, we'll automatically delete everything in it"
                        .to_string()
                        .color(OckamColor::FmtWARNBackground.color())
                ))?
                .write_line(&fmt_para!(
                    "To learn more about production ready spaces in Ockam Orchestrator, contact us at: {}",
                    "hello@ockam.io".to_string().color(OckamColor::PrimaryResource.color())))?;

            let is_finished = Mutex::new(false);
            let space_name = random_name();
            let create_space = async {
                let space = node.create_space(ctx, &space_name, vec![]).await?;
                *is_finished.lock().await = true;
                Ok(space)
            };

            let message = vec![format!(
                "Creating space {}...",
                space_name
                    .clone()
                    .color(OckamColor::PrimaryResource.color())
            )];
            let progress_output = opts.terminal.progress_output(&message, &is_finished);
            let (space, _) = try_join!(create_space, progress_output)?;
            space
        }
        Some(space) => {
            opts.terminal.write_line(&fmt_log!(
                "Found space {}.",
                space
                    .name
                    .clone()
                    .color(OckamColor::PrimaryResource.color())
            ))?;
            space.clone()
        }
    };
    opts.terminal.write_line(&fmt_ok!(
        "Marked this space as your default space, on this machine.\n"
    ))?;
    Ok(Some(space))
}

async fn get_user_project(
    opts: &CommandGlobalOpts,
    ctx: &Context,
    node: &InMemoryNode,
    user_account_only: bool,
    space: &Space,
) -> Result<Option<Project>> {
    // Get available project for the given space
    opts.terminal.write_line(&fmt_log!(
        "Getting available projects in space {}...",
        space
            .name
            .to_string()
            .color(OckamColor::PrimaryResource.color())
    ))?;

    let is_finished = Mutex::new(false);
    let get_projects = async {
        let projects = node.get_admin_projects(ctx).await?;
        *is_finished.lock().await = true;
        Ok(projects)
    };

    let message = vec![format!("Checking for any existing projects...")];
    let progress_output = opts.terminal.progress_output(&message, &is_finished);

    let (projects, _) = try_join!(get_projects, progress_output)?;

    // If the space has no projects, create one
    let project = match projects.first() {
        None => {
            if user_account_only {
                opts.terminal.write_line(&fmt_para!(
                    "No projects are defined in the space {}.",
                    space
                        .name
                        .to_string()
                        .color(OckamColor::PrimaryResource.color())
                ))?;
                return Ok(None);
            }

            opts.terminal
                .write_line(&fmt_para!(
                    "No projects are defined in the space {}.",
                    space
                        .name
                        .to_string()
                        .color(OckamColor::PrimaryResource.color())
                ))?
                .write_line(&fmt_para!("Creating a project for you..."))?;

            let is_finished = Mutex::new(false);
            let project_name = "default".to_string();
            let get_project = async {
                let project = node
                    .create_project(ctx, &space.name, &project_name, vec![])
                    .await?;
                *is_finished.lock().await = true;
                Ok(project)
            };

            let message = vec![format!(
                "Creating project {}...",
                project_name
                    .to_string()
                    .color(OckamColor::PrimaryResource.color())
            )];
            let progress_output = opts.terminal.progress_output(&message, &is_finished);
            let (project, _) = try_join!(get_project, progress_output)?;

            opts.terminal.write_line(&fmt_ok!(
                "Created project {}.",
                project_name
                    .to_string()
                    .color(OckamColor::PrimaryResource.color())
            ))?;

            check_for_project_completion(opts, ctx, node, project).await?
        }
        Some(project) => {
            opts.terminal.write_line(&fmt_log!(
                "Found project {}.",
                project
                    .project_name()
                    .color(OckamColor::PrimaryResource.color())
            ))?;
            project.clone()
        }
    };

    let project = check_project_readiness(opts, ctx, node, project).await?;
    // store the updated project
    opts.state.store_project(project.clone()).await?;
    // set the project as the default one
    opts.state.set_default_trust_context(&project.name).await?;
    opts.state.set_default_project(&project.id).await?;

    opts.terminal.write_line(&fmt_ok!(
        "Marked this project as your default project, on this machine.\n"
    ))?;
    Ok(Some(project))
}