async def _do_greet_device(device, initial_ctx): async with spinner("Waiting for claimer"): in_progress_ctx = await initial_ctx.do_wait_peer() display_greeter_sas = click.style(str(in_progress_ctx.greeter_sas), fg="yellow") click.echo(f"Code to provide to claimer: {display_greeter_sas}") async with spinner("Waiting for claimer"): in_progress_ctx = await in_progress_ctx.do_wait_peer_trust() choices = in_progress_ctx.generate_claimer_sas_choices(size=3) for i, choice in enumerate(choices): display_choice = click.style(choice, fg="yellow") click.echo(f" {i} - {display_choice}") code = await aprompt(f"Select code provided by claimer", type=click.Choice( [str(x) for x in range(len(choices))])) if choices[int(code)] != in_progress_ctx.claimer_sas: click.secho("Wrong code provided", fg="red") return False async with spinner("Waiting for claimer"): in_progress_ctx = await in_progress_ctx.do_signify_trust() in_progress_ctx = await in_progress_ctx.do_get_claim_requests() granted_device_label = await aprompt( "New device label", default=in_progress_ctx.requested_device_label) async with spinner("Creating the device in the backend"): await in_progress_ctx.do_create_new_device( author=device, device_label=granted_device_label) return True
async def _do_claim_device(initial_ctx): async with spinner( "Initializing connection with greeter for claiming device"): in_progress_ctx = await initial_ctx.do_wait_peer() choices = in_progress_ctx.generate_greeter_sas_choices(size=3) for i, choice in enumerate(choices): display_choice = click.style(choice, fg="yellow") click.echo(f" {i} - {display_choice}") code = await aprompt(f"Select code provided by greeter", type=click.Choice( [str(x) for x in range(len(choices))])) if choices[int(code)] != in_progress_ctx.greeter_sas: click.secho("Wrong code provided", fg="red") return None in_progress_ctx = await in_progress_ctx.do_signify_trust() display_claimer_sas = click.style(str(in_progress_ctx.claimer_sas), fg="yellow") click.echo(f"Code to provide to greeter: {display_claimer_sas}") async with spinner("Waiting for greeter"): in_progress_ctx = await in_progress_ctx.do_wait_peer_trust() requested_device_label = await aprompt("Device label", default=platform.node()) async with spinner("Waiting for greeter (finalizing)"): new_device = await in_progress_ctx.do_claim_device( requested_device_label=requested_device_label) return new_device
async def _do_greet_user(device, initial_ctx): async with spinner("Waiting for claimer"): in_progress_ctx = await initial_ctx.do_wait_peer() display_greeter_sas = click.style(str(in_progress_ctx.greeter_sas), fg="yellow") click.echo(f"Code to provide to claimer: {display_greeter_sas}") async with spinner("Waiting for claimer"): in_progress_ctx = await in_progress_ctx.do_wait_peer_trust() choices = in_progress_ctx.generate_claimer_sas_choices(size=3) for i, choice in enumerate(choices): display_choice = click.style(choice, fg="yellow") click.echo(f" {i} - {display_choice}") code = await aprompt( f"Select code provided by claimer", type=click.Choice([str(i) for i, _ in enumerate(choices)]), ) if choices[int(code)] != in_progress_ctx.claimer_sas: click.secho("Wrong code provided", fg="red") return False async with spinner("Waiting for claimer"): in_progress_ctx = await in_progress_ctx.do_signify_trust() in_progress_ctx = await in_progress_ctx.do_get_claim_requests() granted_label = await aprompt( "New user label", default=in_progress_ctx.requested_human_handle.label) granted_email = await aprompt( "New user email", default=in_progress_ctx.requested_human_handle.email) granted_device_label = await aprompt( "New user device label", default=in_progress_ctx.requested_device_label) choices = list(UserProfile) for i, choice in enumerate(UserProfile): display_choice = click.style(choice.value, fg="yellow") click.echo(f" {i} - {display_choice}") choice_index = await aprompt("New user profile", default="0", type=click.Choice( [str(i) for i, _ in enumerate(choices)])) granted_profile = choices[int(choice_index)] async with spinner("Creating the user in the backend"): await in_progress_ctx.do_create_new_user( author=device, device_label=granted_device_label, human_handle=HumanHandle(email=granted_email, label=granted_label), profile=granted_profile, ) return True
async def _pki_enrollment_submit( config: CoreConfig, addr: BackendPkiEnrollmentAddr, requested_device_label: DeviceLabel, force: bool, ): ctx = await PkiEnrollmentSubmitterInitialCtx.new(addr) x509_display = f"Certificate SHA1 Fingerprint: " + click.style( ctx.x509_certificate.certificate_sha1.hex(), fg="yellow" ) x509_display += "\nCertificate Issuer Common Name: " + click.style( ctx.x509_certificate.issuer_common_name, fg="yellow" ) x509_display += "\nCertificate Subject Common Name: " + click.style( ctx.x509_certificate.subject_common_name, fg="yellow" ) x509_display += "\nCertificate Subject Email Address: " + click.style( ctx.x509_certificate.subject_email_address, fg="yellow" ) click.echo(x509_display) async with spinner("Sending PKI enrollment to the backend"): ctx = await ctx.submit( config_dir=config.config_dir, requested_device_label=requested_device_label, force=force ) enrollment_id_display = click.style(ctx.enrollment_id.hex, fg="green") click.echo(f"PKI enrollment {enrollment_id_display} submitted")
async def _bootstrap_organization( debug, device_id, organization_bootstrap_addr, config_dir, force, password ): root_signing_key = SigningKey.generate() root_verify_key = root_signing_key.verify_key organization_addr = organization_bootstrap_addr.generate_organization_addr(root_verify_key) device_display = click.style(device_id, fg="yellow") device = generate_new_device(device_id, organization_addr) with operation(f"Creating locally {device_display}"): save_device_with_password(config_dir, device, password, force=force) now = pendulum.now() certified_user = certify_user(None, root_signing_key, device.user_id, device.public_key, now) certified_device = certify_device(None, root_signing_key, device_id, device.verify_key, now) async with spinner(f"Sending {device_display} to server"): async with backend_anonymous_cmds_factory(organization_bootstrap_addr) as cmds: await cmds.organization_bootstrap( organization_bootstrap_addr.organization_id, organization_bootstrap_addr.bootstrap_token, root_verify_key, certified_user, certified_device, ) organization_addr_display = click.style(organization_addr, fg="yellow") click.echo(f"Organization url: {organization_addr_display}")
async def _greet_invitation(config, device, token): async with backend_authenticated_cmds_factory( addr=device.organization_addr, device_id=device.device_id, signing_key=device.signing_key, keepalive=config.backend_connection_keepalive, ) as cmds: async with spinner("Retrieving invitation info"): rep = await cmds.invite_list() if rep["status"] != "ok": raise RuntimeError(f"Backend error: {rep}") for invitation in rep["invitations"]: if invitation["token"] == token: break else: raise RuntimeError(f"Invitation not found") if invitation["type"] == InvitationType.USER: initial_ctx = UserGreetInitialCtx(cmds=cmds, token=token) do_greet = partial(_do_greet_user, device, initial_ctx) else: assert invitation["type"] == InvitationType.DEVICE initial_ctx = DeviceGreetInitialCtx(cmds=cmds, token=token) do_greet = partial(_do_greet_device, device, initial_ctx) while True: try: greet_done = await do_greet() if greet_done: break except InviteError as exc: click.secho(str(exc), fg="red") click.secho("Restarting the invitation process", fg="red")
async def _bootstrap_organization( config: CoreConfig, addr: BackendOrganizationBootstrapAddr, device_label: Optional[DeviceLabel], human_label: Optional[str], human_email: Optional[str], save_device_with_selected_auth: Callable, ) -> None: if not human_label: human_label = await aprompt("User fullname") if not human_email: human_email = await aprompt("User email") human_handle = HumanHandle(email=human_email, label=human_label) if not device_label: device_label_raw = await aprompt("Device label", default=platform.node()) device_label = DeviceLabel(device_label_raw) async with spinner("Bootstrapping organization in the backend"): new_device = await do_bootstrap_organization(addr, human_handle=human_handle, device_label=device_label) # We don't have to worry about overwritting an existing keyfile # given their names are base on the device's slughash which is intended # to be globally unique. # The organization is brand new, of course there is no existing # remote user manifest, hence our placeholder is non-speculative. await user_storage_non_speculative_init(data_base_dir=config.data_base_dir, device=new_device) await save_device_with_selected_auth(config_dir=config.config_dir, device=new_device)
async def _invite_user( config: CoreConfig, device: LocalDevice, email: str, send_email: bool ) -> None: async with spinner("Creating user invitation"): async with backend_authenticated_cmds_factory( addr=device.organization_addr, device_id=device.device_id, signing_key=device.signing_key, keepalive=config.backend_connection_keepalive, ) as cmds: rep = await cmds.invite_new( type=InvitationType.USER, claimer_email=email, send_email=send_email ) if rep["status"] != "ok": raise RuntimeError(f"Backend refused to create user invitation: {rep}") if send_email and "email_sent" in rep: if rep["email_sent"] != InvitationEmailSentStatus.SUCCESS: click.secho("Email could not be sent", fg="red") action_addr = BackendInvitationAddr.build( backend_addr=device.organization_addr.get_backend_addr(), organization_id=device.organization_id, invitation_type=InvitationType.USER, token=rep["token"], ) action_addr_display = click.style(action_addr.to_url(), fg="yellow") click.echo(f"url: {action_addr_display}")
async def _claim_invitation(config, addr, password): async with backend_invited_cmds_factory(addr=addr) as cmds: try: async with spinner("Retrieving invitation info"): initial_ctx = await claimer_retrieve_info(cmds) except BackendConnectionRefused: raise RuntimeError("Invitation not found") if initial_ctx.greeter_human_handle: display_greeter = click.style(initial_ctx.greeter_human_handle, fg="yellow") else: display_greeter = click.style(initial_ctx.greeter_user_id, fg="yellow") click.echo(f"Invitation greeter: {display_greeter}") while True: try: if isinstance(initial_ctx, DeviceClaimInitialCtx): new_device = await _do_claim_device(initial_ctx) else: assert isinstance(initial_ctx, UserClaimInitialCtx) new_device = await _do_claim_user(initial_ctx) if new_device: break except InviteError as exc: click.secho(str(exc), fg="red") click.secho("Restarting the invitation process", fg="red") device_display = click.style(new_device.slughash, fg="yellow") with operation(f"Saving device {device_display}"): save_device_with_password(config.config_dir, new_device, password)
async def _create_organization(debug, name, backend_addr, administrator_token): async with spinner("Creating organization in backend"): async with backend_administrator_cmds_factory(backend_addr, administrator_token) as cmds: bootstrap_token = await cmds.organization_create(name) organization_addr = BackendOrganizationBootstrapAddr.build(backend_addr, name, bootstrap_token) organization_addr_display = click.style(organization_addr, fg="yellow") click.echo(f"Bootstrap organization url: {organization_addr_display}")
async def _bootstrap_organization(debug, device_id, organization_bootstrap_addr, config_dir, force, password): root_signing_key = SigningKey.generate() root_verify_key = root_signing_key.verify_key organization_addr = organization_bootstrap_addr.generate_organization_addr( root_verify_key) device_display = click.style(device_id, fg="yellow") device = generate_new_device(device_id, organization_addr, profile=UserProfile.ADMIN) with operation(f"Creating locally {device_display}"): save_device_with_password(config_dir, device, password, force=force) now = pendulum.now() user_certificate = UserCertificateContent( author=None, timestamp=now, user_id=device.user_id, public_key=device.public_key, profile=device.profile, ) redacted_user_certificate = user_certificate.evolve(human_handle=None) device_certificate = DeviceCertificateContent(author=None, timestamp=now, device_id=device_id, verify_key=device.verify_key) redacted_device_certificate = device_certificate.evolve(device_label=None) user_certificate = user_certificate.dump_and_sign(root_signing_key) device_certificate = device_certificate.dump_and_sign(root_signing_key) redacted_user_certificate = redacted_user_certificate.dump_and_sign( root_signing_key) redacted_device_certificate = redacted_device_certificate.dump_and_sign( root_signing_key) async with spinner(f"Sending {device_display} to server"): async with apiv1_backend_anonymous_cmds_factory( organization_bootstrap_addr) as cmds: await cmds.organization_bootstrap( organization_id=organization_bootstrap_addr.organization_id, bootstrap_token=organization_bootstrap_addr.token, root_verify_key=root_verify_key, user_certificate=user_certificate, device_certificate=device_certificate, redacted_user_certificate=redacted_user_certificate, redacted_device_certificate=redacted_device_certificate, ) organization_addr_display = click.style(organization_addr.to_url(), fg="yellow") click.echo(f"Organization url: {organization_addr_display}")
async def _create_organization(organization_id: OrganizationID, backend_addr: BackendAddr, administration_token: str) -> None: async with spinner("Creating organization in backend"): bootstrap_token = await create_organization_req( organization_id, backend_addr, administration_token) organization_addr = BackendOrganizationBootstrapAddr.build( backend_addr, organization_id, bootstrap_token) organization_addr_display = click.style(organization_addr.to_url(), fg="yellow") click.echo(f"Bootstrap organization url: {organization_addr_display}")
async def _migrate(db): async with spinner("Migrate"): result = await apply_migrations(db, migrations, dry_run) for migration in result.already_applied: click.secho(f"{migration.file_name} (already applied)", fg="white") for migration in result.new_apply: click.secho(f"{migration.file_name} {ok}", fg="green") if result.error: migration, msg = result.error click.secho(f"{migration.file_name} {ko}: {msg}", fg="red")
async def _do_greet_user(device: LocalDevice, initial_ctx: UserGreetInitialCtx) -> bool: async with spinner("Waiting for claimer"): in_progress_ctx = await initial_ctx.do_wait_peer() display_greeter_sas = click.style(str(in_progress_ctx.greeter_sas), fg="yellow") click.echo(f"Code to provide to claimer: {display_greeter_sas}") async with spinner("Waiting for claimer"): in_progress_ctx = await in_progress_ctx.do_wait_peer_trust() choices = in_progress_ctx.generate_claimer_sas_choices(size=3) for i, choice in enumerate(choices): display_choice = click.style(choice, fg="yellow") click.echo(f" {i} - {display_choice}") code = await aprompt( f"Select code provided by claimer", type=click.Choice([str(i) for i, _ in enumerate(choices)]), ) if choices[int(code)] != in_progress_ctx.claimer_sas: click.secho("Wrong code provided", fg="red") return False async with spinner("Waiting for claimer"): in_progress_ctx = await in_progress_ctx.do_signify_trust() in_progress_ctx = await in_progress_ctx.do_get_claim_requests() granted_device_label, granted_human_handle, granted_profile = await ask_info_new_user( default_device_label=in_progress_ctx.requested_device_label, default_user_label=in_progress_ctx.requested_human_handle.label, default_user_email=in_progress_ctx.requested_human_handle.email, ) async with spinner("Creating the user in the backend"): await in_progress_ctx.do_create_new_user( author=device, device_label=granted_device_label, human_handle=granted_human_handle, profile=granted_profile, ) return True
async def _claim_device(config, organization_addr, new_device_id, token, password): async with spinner("Waiting for referee to reply"): device = await actual_claim_device( organization_addr=organization_addr, new_device_id=new_device_id, token=token, keepalive=config.backend_connection_keepalive, ) device_display = click.style(new_device_id, fg="yellow") with operation(f"Saving locally {device_display}"): save_device_with_password(config.config_dir, device, password)
async def _create_organization(debug, name, backend_addr, administration_token, expiration_date): async with spinner("Creating organization in backend"): async with apiv1_backend_administration_cmds_factory( backend_addr, administration_token ) as cmds: rep = await cmds.organization_create(name, expiration_date) if rep["status"] != "ok": raise RuntimeError(f"Backend refused to create organization: {rep}") bootstrap_token = rep["bootstrap_token"] organization_addr = BackendOrganizationBootstrapAddr.build(backend_addr, name, bootstrap_token) organization_addr_display = click.style(organization_addr.to_url(), fg="yellow") click.echo(f"Bootstrap organization url: {organization_addr_display}")
async def _export_recovery_device(config: CoreConfig, original_device: LocalDevice, output: Path) -> None: async with spinner("Creating new recovery device"): recovery_device = await generate_recovery_device(original_device) file_name = get_recovery_device_file_name(recovery_device) file_path = output / file_name file_path_display = click.style(str(file_path.absolute()), fg="yellow") with operation(f"Saving recovery device file in {file_path_display}"): passphrase = await save_recovery_device(file_path, recovery_device) p1 = click.style("Save the recovery passphrase in a safe place:", fg="red") p2 = click.style(passphrase, fg="green") click.echo(f"{p1} {p2}")
async def _migrate(db): async with spinner("Migrate"): result = await migrate_db(db, migrations, dry_run) for key, values in zip(result._fields, result): color = result_colors.get(key, "white") if key == "error": if values: _echo_migration(values[0], color) raise click.ClickException(values[1]) else: is_done = False for value in values: if key in ["already_applied", "new_apply"]: is_done = True else: is_done = False _echo_migration(value, color, is_done)
async def _claim_user(config, backend_addr, token, new_device_id, password, pkcs11): async with backend_anonymous_cmds_factory(backend_addr) as cmds: async with spinner("Waiting for referee to reply"): device = await actual_claim_user(cmds, new_device_id, token) device_display = click.style(new_device_id, fg="yellow") with operation(f"Saving locally {device_display}"): if pkcs11: token_id = click.prompt("PCKS11 token id", type=int) key_id = click.prompt("PCKS11 key id", type=int) save_device_with_pkcs11(config.config_dir, device, token_id, key_id) else: save_device_with_password(config.config_dir, device, password)
async def _import_recovery_device( config: CoreConfig, recovery_file: Path, passphrase: str, new_device_label: DeviceLabel, save_device_with_selected_auth: Callable, ) -> None: recovery_device = await load_recovery_device(recovery_file, passphrase) device_label_display = click.style(new_device_label, fg="yellow") async with spinner(f"Creating new device {device_label_display}"): new_device = await generate_new_device_from_recovery( recovery_device, new_device_label) await save_device_with_selected_auth(config_dir=config.config_dir, device=new_device)
async def _invite_user(config, device, invited_user_id, admin): async with backend_cmds_factory(device.organization_addr, device.device_id, device.signing_key) as cmds: token = generate_invitation_token() organization_addr_display = click.style(device.organization_addr, fg="yellow") token_display = click.style(token, fg="yellow") click.echo(f"Backend url: {organization_addr_display}") click.echo(f"Invitation token: {token_display}") async with spinner("Waiting for invitation reply"): invite_device_id = await invite_and_create_user( device, cmds, invited_user_id, token, admin) display_device = click.style(invite_device_id, fg="yellow") click.echo(f"Device {display_device} has been created")
async def _bootstrap_organization(config, addr, password, force): label = await aprompt("User fullname") email = await aprompt("User email") human_handle = HumanHandle(email=email, label=label) device_label = await aprompt("Device label", default=platform.node()) async with apiv1_backend_anonymous_cmds_factory(addr=addr) as cmds: async with spinner("Bootstrapping organization in the backend"): new_device = await do_bootstrap_organization( cmds=cmds, human_handle=human_handle, device_label=device_label) device_display = click.style(new_device.slughash, fg="yellow") with operation(f"Saving device {device_display}"): save_device_with_password(config_dir=config.config_dir, device=new_device, password=password, force=force)
async def _invite_device(config, device, new_device_name): token = generate_invitation_token() organization_addr_display = click.style(device.organization_addr, fg="yellow") token_display = click.style(token, fg="yellow") click.echo(f"Backend url: {organization_addr_display}") click.echo(f"Invitation token: {token_display}") async with backend_cmds_factory(device.organization_addr, device.device_id, device.signing_key) as cmds: async with spinner("Waiting for invitation reply"): await invite_and_create_device(device, cmds, new_device_name, token) display_device = click.style(f"{device.device_name}@{new_device_name}", fg="yellow") click.echo(f"Device {display_device} is ready !")
async def _invite_device(config, device): async with spinner("Creating device invitation"): async with backend_authenticated_cmds_factory( addr=device.organization_addr, device_id=device.device_id, signing_key=device.signing_key, keepalive=config.backend_connection_keepalive, ) as cmds: rep = await cmds.invite_new(type=InvitationType.DEVICE) if rep["status"] != "ok": raise RuntimeError(f"Backend refused to create device invitation: {rep}") action_addr = BackendInvitationAddr.build( backend_addr=device.organization_addr, organization_id=device.organization_id, invitation_type=InvitationType.DEVICE, token=rep["token"], ) action_addr_display = click.style(action_addr.to_url(), fg="yellow") click.echo(f"url: {action_addr_display}")
async def _invite_user(config, device, invited_user_id, admin): action_addr = BackendOrganizationClaimUserAddr.build( organization_addr=device.organization_addr, user_id=invited_user_id) token = generate_invitation_token() action_addr_display = click.style(action_addr.to_url(), fg="yellow") token_display = click.style(token, fg="yellow") click.echo(f"url: {action_addr_display}") click.echo(f"token: {token_display}") async with spinner("Waiting for invitation reply"): invite_device_id = await invite_and_create_user( device=device, user_id=invited_user_id, token=token, is_admin=admin, keepalive=config.backend_connection_keepalive, ) display_device = click.style(invite_device_id, fg="yellow") click.echo(f"Device {display_device} has been created")
async def _bootstrap_organization(config, addr, password, device_label, human_label, human_email): if not human_label: human_label = await aprompt("User fullname") if not human_email: human_email = await aprompt("User email") human_handle = HumanHandle(email=human_email, label=human_label) if not device_label: device_label = await aprompt("Device label", default=platform.node()) async with apiv1_backend_anonymous_cmds_factory(addr=addr) as cmds: async with spinner("Bootstrapping organization in the backend"): new_device = await do_bootstrap_organization( cmds=cmds, human_handle=human_handle, device_label=device_label ) device_display = click.style(new_device.slughash, fg="yellow") # We don't have to worry about overwritting an existing keyfile # given their names are base on the device's slughash which is intended # to be globally unique. with operation(f"Saving device {device_display}"): save_device_with_password( config_dir=config.config_dir, device=new_device, password=password )
async def _invite_device(config, device, new_device_name): action_addr = BackendOrganizationClaimDeviceAddr.build( organization_addr=device.organization_addr, device_id=DeviceID(f"{device.user_id}@{new_device_name}"), ) token = generate_invitation_token() action_addr_display = click.style(action_addr.to_url(), fg="yellow") token_display = click.style(token, fg="yellow") click.echo(f"url: {action_addr_display}") click.echo(f"token: {token_display}") async with spinner("Waiting for invitation reply"): await invite_and_create_device( device=device, new_device_name=new_device_name, token=token, keepalive=config.backend_connection_keepalive, ) display_device = click.style(f"{device.device_name}@{new_device_name}", fg="yellow") click.echo(f"Device {display_device} is ready !")
async def _claim_invitation( config: CoreConfig, addr: BackendInvitationAddr, save_device_with_selected_auth: Callable ) -> None: async with backend_invited_cmds_factory( addr=addr, keepalive=config.backend_connection_keepalive ) as cmds: try: async with spinner("Retrieving invitation info"): initial_ctx = await claimer_retrieve_info(cmds) except BackendConnectionRefused: raise RuntimeError("Invitation not found") if initial_ctx.greeter_human_handle: display_greeter = click.style(str(initial_ctx.greeter_human_handle), fg="yellow") else: display_greeter = click.style(initial_ctx.greeter_user_id, fg="yellow") click.echo(f"Invitation greeter: {display_greeter}") while True: try: if isinstance(initial_ctx, DeviceClaimInitialCtx): new_device = await _do_claim_device(initial_ctx) else: assert isinstance(initial_ctx, UserClaimInitialCtx) new_device = await _do_claim_user(initial_ctx) if new_device: break except InviteError as exc: click.secho(str(exc), fg="red") click.secho("Restarting the invitation process", fg="red") # Claiming a user means we are it first device, hence we know there # is no existing user manifest (hence our placeholder is non-speculative) if addr.invitation_type == InvitationType.USER: await user_storage_non_speculative_init( data_base_dir=config.data_base_dir, device=new_device ) await save_device_with_selected_auth(config_dir=config.config_dir, device=new_device)
async def _pki_enrollment_poll( config: CoreConfig, enrollment_id_filter: Optional[str], dry_run: bool, save_device_with_selected_auth: Callable, finalize: Sequence[str], ): pendings = PkiEnrollmentSubmitterSubmittedCtx.list_from_disk(config_dir=config.config_dir) # Try to shorten the UUIDs to make it easier to work with enrollment_ids = [e.enrollment_id for e in pendings] for enrollment_id_len in range(3, 64): if len({h.hex[:enrollment_id_len] for h in enrollment_ids}) == len(enrollment_ids): break # Manage pre-selected actions preselected_actions = {x: "finalize" for x in finalize} def _preselected_actions_lookup(enrollment_id: UUID) -> Optional[str]: for preselected in preselected_actions: if len(preselected) < enrollment_id_len: continue if enrollment_id.hex.startswith(preselected): return preselected_actions.pop(preselected) # Filter if needed if enrollment_id_filter: if len(enrollment_id_filter) < enrollment_id_len: raise RuntimeError() pendings = [e for e in pendings if e.enrollment_id.hex.starswith(enrollment_id_filter)] if not pendings: raise RuntimeError(f"No enrollment with id {enrollment_id_filter} locally available") def _display_pending_enrollment(pending: PkiEnrollmentSubmitterSubmittedCtx): enrollment_id_display = click.style( pending.enrollment_id.hex[:enrollment_id_len], fg="green" ) display = f"Pending enrollment {enrollment_id_display}" display += f"\n Submitted on: " + click.style(pending.submitted_on, fg="yellow") display += f"\n Organization URL: " + click.style(pending.addr, fg="yellow") display += f"\n Certificate SHA1 Fingerprint: " + click.style( pending.x509_certificate.certificate_sha1.hex(), fg="yellow" ) display += "\n Certificate Issuer Common Name: " + click.style( pending.x509_certificate.issuer_common_name, fg="yellow" ) display += "\n Certificate Subject Common Name: " + click.style( pending.x509_certificate.subject_common_name, fg="yellow" ) display += "\n Certificate Subject Email Address: " + click.style( pending.x509_certificate.subject_email_address, fg="yellow" ) display += "\n Requested Device Label: " + click.style( pending.submit_payload.requested_device_label, fg="yellow" ) return display def _display_accepted_enrollment(accepted: PkiEnrollmentSubmitterAcceptedStatusCtx): display = ( f"Enrollment has been accepted on " + click.style(ctx.accepted_on, fg="yellow") + " by:" ) display += f"\n Certificate SHA1 Fingerprint: " + click.style( accepted.accepter_x509_certificate.certificate_sha1.hex(), fg="yellow" ) display += "\n Certificate Issuer Common Name: " + click.style( accepted.accepter_x509_certificate.issuer_common_name, fg="yellow" ) display += "\n Certificate Subject Common Name: " + click.style( accepted.accepter_x509_certificate.subject_common_name, fg="yellow" ) display += "\n Certificate Subject Email Address: " + click.style( accepted.accepter_x509_certificate.subject_email_address, fg="yellow" ) return display num_pendings_display = click.style(str(len(pendings)), fg="green") click.echo(f"Found {num_pendings_display} pending enrollment(s):") for ctx in pendings: click.echo(_display_pending_enrollment(ctx)) async with spinner("Fetching PKI enrollment status from the backend"): try: ctx = await ctx.poll(extra_trust_roots=config.pki_extra_trust_roots) except Exception: # TODO: exception handling ! raise if isinstance(ctx, PkiEnrollmentSubmitterSubmittedStatusCtx): # Nothing to do click.echo("Enrollment is still pending") elif isinstance(ctx, PkiEnrollmentSubmitterCancelledStatusCtx): click.echo( "Enrollment has been cancelled on " + click.style(ctx.submitted_on, fg="yellow") ) if not dry_run: await ctx.remove_from_disk() elif isinstance(ctx, PkiEnrollmentSubmitterRejectedStatusCtx): click.echo( "Enrollment has been rejected on " + click.style(ctx.rejected_on, fg="yellow") ) if not dry_run: await ctx.remove_from_disk() elif isinstance(ctx, PkiEnrollmentSubmitterAcceptedStatusButBadSignatureCtx): click.echo( "Enrollment has been accepted on " + click.style(ctx.accepted_on, fg="yellow") ) raise RuntimeError( f"Cannot validate accept information with selected X509 certificate: {ctx.error}" ) else: assert isinstance(ctx, PkiEnrollmentSubmitterAcceptedStatusCtx) click.echo(_display_accepted_enrollment(ctx)) if not dry_run: preselected_finalize = _preselected_actions_lookup(ctx.enrollment_id) == "finalize" if not preselected_finalize and not await aconfirm("Finalize device creation"): return ctx = await ctx.finalize() await save_device_with_smartcard_in_config( config.config_dir, ctx.new_device, certificate_id=ctx.x509_certificate.certificate_id, certificate_sha1=ctx.x509_certificate.certificate_sha1, ) await ctx.remove_from_disk()
async def _pki_enrollment_review_pendings( config: CoreConfig, device: LocalDevice, list_only: bool, accept: Sequence[str], reject: Sequence[str], ): # Connect to the backend async with backend_authenticated_cmds_factory( addr=device.organization_addr, device_id=device.device_id, signing_key=device.signing_key, keepalive=config.backend_connection_keepalive, ) as cmds: pendings = await accepter_list_submitted_from_backend( cmds=cmds, extra_trust_roots=config.pki_extra_trust_roots ) num_pendings_display = click.style(str(len(pendings)), fg="green") click.echo(f"Found {num_pendings_display} pending enrollment(s):") # Try to shorten the UUIDs to make it easier to work with enrollment_ids = [e.enrollment_id for e in pendings] for enrollment_id_len in range(3, 64): if len({h.hex[:enrollment_id_len] for h in enrollment_ids}) == len(enrollment_ids): break preselected_actions = {**{x: "accept" for x in accept}, **{x: "reject" for x in reject}} def _preselected_actions_lookup(enrollment_id: UUID) -> Optional[str]: for preselected in preselected_actions: if len(preselected) < enrollment_id_len: continue if enrollment_id.hex.startswith(preselected): return preselected_actions.pop(preselected) def _display_pending_enrollment(pending): enrollment_id_display = click.style( pending.enrollment_id.hex[:enrollment_id_len], fg="green" ) display = f"Pending enrollment {enrollment_id_display}" display += f"\n Submitted on: " + click.style(pending.submitted_on, fg="yellow") display += f"\n Certificate SHA1 fingerprint: " + click.style( pending.submitter_x509_certificate_sha1.hex(), fg="yellow" ) if isinstance(pending, PkiEnrollementAccepterInvalidSubmittedCtx): display += click.style("\nInvalid enrollment", fg="red") + f": {pending.error}" else: assert isinstance(pending, PkiEnrollementAccepterValidSubmittedCtx) display += "\n Certificate Issuer Common Name: " + click.style( pending.submitter_x509_certificate.issuer_common_name, fg="yellow" ) display += "\n Certificate Subject Common Name: " + click.style( pending.submitter_x509_certificate.subject_common_name, fg="yellow" ) display += "\n Certificate Subject Email Address: " + click.style( pending.submitter_x509_certificate.subject_email_address, fg="yellow" ) display += "\n Requested Device Label: " + click.style( pending.submit_payload.requested_device_label, fg="yellow" ) return display for pending in pendings: enrollment_id = pending.enrollment_id click.echo(_display_pending_enrollment(pending)) if list_only: continue if isinstance(pending, PkiEnrollementAccepterInvalidSubmittedCtx): action = _preselected_actions_lookup(enrollment_id) if action == "accept": raise RuntimeError(f"Could not accept invalid enrollment {enrollment_id.hex}") elif action is None: # Let the user pick and action action = await aprompt( "-> Action", default="Ignore", type=click.Choice(["Ignore", "Reject"], case_sensitive=False), ) action = action.lower() if action == "ignore": continue else: # Reject the request assert action == "reject" async with spinner("Rejecting PKI enrollment from the backend"): await pending.reject() continue else: assert isinstance(pending, PkiEnrollementAccepterValidSubmittedCtx) action = _preselected_actions_lookup(enrollment_id) if action is None: # Let the user pick and action action = await aprompt( "-> Action", default="Ignore", type=click.Choice(["Ignore", "Reject", "Accept"], case_sensitive=False), ) action = action.lower() if action == "ignore": continue # Reject the request if action == "reject": async with spinner("Rejecting PKI enrollment from the backend"): await pending.reject() continue else: # Accept the request assert action == "accept" # Let the admin edit the user information granted_device_label, granted_human_handle, granted_profile = await ask_info_new_user( default_device_label=pending.submit_payload.requested_device_label, default_user_label=pending.submitter_x509_certificate.subject_common_name, default_user_email=pending.submitter_x509_certificate.subject_email_address, ) async with spinner("Accepting PKI enrollment in the backend"): await pending.accept( author=device, device_label=granted_device_label, human_handle=granted_human_handle, profile=granted_profile, ) if preselected_actions: raise RuntimeError( f"Additional --accept/--reject elements not used: {', '.join(preselected_actions.keys())}" )