user link multiple primary login methods

This commit is contained in:
mbecker20
2025-12-12 17:08:03 -08:00
parent c91963fdbb
commit de83b3a5a0
32 changed files with 2081 additions and 1085 deletions

View File

@@ -48,6 +48,91 @@ pub type SetLastSeenUpdateResponse = NoData;
//
/// Begin linking flow for a third party login. Response: [NoData].
///
/// First call this method when authenticated, then
/// redirect user to /api/auth/{provider}/link.
///
/// 'provider' can be:
/// - github
/// - google
/// - oidc
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
)]
#[empty_traits(KomodoUserRequest)]
#[response(BeginThirdPartyLoginLinkResponse)]
#[error(serror::Error)]
pub struct BeginThirdPartyLoginLink {}
#[typeshare]
pub type BeginThirdPartyLoginLinkResponse = NoData;
//
/// Unlink a login. Response: [NoData].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
)]
#[empty_traits(KomodoUserRequest)]
#[response(UnlinkLoginResponse)]
#[error(serror::Error)]
pub struct UnlinkLogin {
/// 'provider' can be:
/// - Local
/// - Github
/// - Google
/// - Oidc
pub provider: String,
}
#[typeshare]
pub type UnlinkLoginResponse = NoData;
//
/// Update the calling users username.
/// Response: [NoData].
///
/// Will fail if the new username is invalid or already taken.
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
)]
#[empty_traits(KomodoUserRequest)]
#[response(UpdateUsernameResponse)]
#[error(serror::Error)]
pub struct UpdateUsername {
pub username: String,
}
#[typeshare]
pub type UpdateUsernameResponse = NoData;
//
/// Update the calling user's password. Response: [NoData].
///
/// If the User was created using third party login method,
/// using [UpdatePassword] adds or updates the Local linked (additional) login method.
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
)]
#[empty_traits(KomodoUserRequest)]
#[response(UpdatePasswordResponse)]
#[error(serror::Error)]
pub struct UpdatePassword {
pub password: String,
}
#[typeshare]
pub type UpdatePasswordResponse = NoData;
//
/// Create an api key for the calling user.
/// Response: [CreateApiKeyResponse].
///

View File

@@ -3,48 +3,10 @@ use resolver_api::Resolve;
use serde::{Deserialize, Serialize};
use typeshare::typeshare;
use crate::entities::{NoData, user::User};
use crate::entities::user::User;
use super::KomodoWriteRequest;
//
/// **Only for local users**. Update the calling users username.
/// Response: [NoData].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
)]
#[empty_traits(KomodoWriteRequest)]
#[response(UpdateUserUsernameResponse)]
#[error(serror::Error)]
pub struct UpdateUserUsername {
pub username: String,
}
#[typeshare]
pub type UpdateUserUsernameResponse = NoData;
//
/// **Only for local users**. Update the calling users password.
/// Response: [NoData].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
)]
#[empty_traits(KomodoWriteRequest)]
#[response(UpdateUserPasswordResponse)]
#[error(serror::Error)]
pub struct UpdateUserPassword {
pub password: String,
}
#[typeshare]
pub type UpdateUserPasswordResponse = NoData;
//
/// **Admin only**. Delete a user.
/// Admins can delete any non-admin user.
/// Only Super Admin can delete an admin.

View File

@@ -1,8 +1,11 @@
use std::{collections::HashMap, sync::OnceLock};
use anyhow::anyhow;
use base64urlsafedata::Base64UrlSafeData;
use derive_variants::{EnumVariants, ExtractVariant};
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use strum::{AsRefStr, Display, EnumString};
use typeshare::typeshare;
use webauthn_rs::prelude::Passkey;
@@ -65,9 +68,14 @@ pub struct User {
#[serde(default)]
pub create_build_permissions: bool,
/// The user-type specific config.
/// The primary user login.
pub config: UserConfig,
/// Additional linked login methods.
/// May not contain 'Service' type config.
#[serde(default)]
pub linked_logins: LinkedLoginsMap,
/// TOTP 2fa credentials
#[serde(default)]
pub totp: UserTotpConfig,
@@ -94,7 +102,22 @@ pub struct User {
}
#[typeshare]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, EnumVariants)]
#[variant_derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Serialize,
Deserialize,
Display,
EnumString,
AsRefStr
)]
#[serde(tag = "type", content = "data")]
pub enum UserConfig {
/// User that logs in with username / password
@@ -121,6 +144,42 @@ impl Default for UserConfig {
}
}
impl UserConfig {
pub fn sanitize(&mut self) {
if let UserConfig::Local { password } = self {
password.clear();
}
}
}
#[typeshare]
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct LinkedLoginsMap(HashMap<UserConfigVariant, UserConfig>);
impl LinkedLoginsMap {
pub fn get(
&self,
variant: UserConfigVariant,
) -> Option<&UserConfig> {
self.0.get(&variant)
}
pub fn update(&mut self, login: UserConfig) -> anyhow::Result<()> {
if let UserConfig::Service { .. } = &login {
return Err(anyhow!(
"Cannot insert Service type configuration as additional login method."
));
}
let key = login.extract_variant();
self.0.insert(key, login);
Ok(())
}
pub fn remove(&mut self, variant: UserConfigVariant) {
self.0.remove(&variant);
}
}
#[typeshare]
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct UserTotpConfig {
@@ -160,9 +219,12 @@ impl UserPasskeyConfig {
impl User {
/// Prepares user object for transport by clearing any sensitive fields
pub fn sanitize(&mut self) {
if let UserConfig::Local { password } = &mut self.config {
password.clear();
}
self.config.sanitize();
self
.linked_logins
.0
.values_mut()
.for_each(UserConfig::sanitize);
self.totp.sanitize();
self.passkey.sanitize();
}

View File

@@ -30,6 +30,7 @@ function fix_types() {
.replaceAll("AlerterEndpointVariant", 'AlerterEndpoint["type"]')
.replaceAll("AlertDataVariant", 'AlertData["type"]')
.replaceAll("ServerTemplateConfigVariant", 'ServerTemplateConfig["type"]')
.replaceAll("UserConfigVariant", 'UserConfig["type"]')
// Add '| string' to env vars
.replaceAll("EnvironmentVar[]", "EnvironmentVar[] | string")
.replaceAll("IndexSet", "Array")

View File

@@ -13,6 +13,8 @@ export type AuthResponses = {
export type UserResponses = {
PushRecentlyViewed: Types.PushRecentlyViewedResponse;
SetLastSeenUpdate: Types.SetLastSeenUpdateResponse;
UpdateUsername: Types.UpdateUsernameResponse;
UpdatePassword: Types.UpdatePasswordResponse;
CreateApiKey: Types.CreateApiKeyResponse;
DeleteApiKey: Types.DeleteApiKeyResponse;
BeginTotpEnrollment: Types.BeginTotpEnrollmentResponse;
@@ -21,6 +23,8 @@ export type UserResponses = {
BeginPasskeyEnrollment: Types.BeginPasskeyEnrollmentResponse;
ConfirmPasskeyEnrollment: Types.ConfirmPasskeyEnrollmentResponse;
UnenrollPasskey: Types.UnenrollPasskeyResponse;
BeginThirdPartyLoginLink: Types.BeginThirdPartyLoginLinkResponse;
UnlinkLogin: Types.UnlinkLoginResponse;
};
export type ReadResponses = {
@@ -319,8 +323,6 @@ export type WriteResponses = {
// ==== USER ====
CreateLocalUser: Types.CreateLocalUserResponse;
UpdateUserUsername: Types.UpdateUserUsernameResponse;
UpdateUserPassword: Types.UpdateUserPasswordResponse;
DeleteUser: Types.DeleteUserResponse;
// ==== SERVICE USER ====

View File

@@ -350,6 +350,8 @@ export type _CreationChallengeResponse = any;
/** Response for [BeginPasskeyEnrollment]. */
export type BeginPasskeyEnrollmentResponse = _CreationChallengeResponse;
export type BeginThirdPartyLoginLinkResponse = NoData;
export enum Operation {
None = "None",
CreateSwarm = "CreateSwarm",
@@ -1102,6 +1104,8 @@ export type UserConfig =
description: string;
}};
export type LinkedLoginsMap = Record<UserConfig["type"], UserConfig>;
export interface UserTotpConfig {
/** TOTP shared secret, encrypted */
secret: string;
@@ -1138,8 +1142,13 @@ export interface User {
create_server_permissions?: boolean;
/** Whether the user has permission to create builds */
create_build_permissions?: boolean;
/** The user-type specific config. */
/** The primary user login. */
config: UserConfig;
/**
* Additional linked login methods.
* May not contain 'Service' type config.
*/
linked_logins?: LinkedLoginsMap;
/** TOTP 2fa credentials */
totp?: UserTotpConfig;
/** WebAuthn Passkey 2fa credentials */
@@ -5481,12 +5490,16 @@ export type UnenrollPasskeyResponse = NoData;
/** Response for [UnenrollTotp]. */
export type UnenrollTotpResponse = NoData;
export type UnlinkLoginResponse = NoData;
export type UpdateDockerRegistryAccountResponse = DockerRegistryAccount;
export type UpdateGitProviderAccountResponse = GitProviderAccount;
export type UpdateOnboardingKeyResponse = OnboardingKey;
export type UpdatePasswordResponse = NoData;
export type UpdatePermissionOnResourceTypeResponse = NoData;
export type UpdatePermissionOnTargetResponse = NoData;
@@ -5501,9 +5514,7 @@ export type UpdateUserAdminResponse = NoData;
export type UpdateUserBasePermissionsResponse = NoData;
export type UpdateUserPasswordResponse = NoData;
export type UpdateUserUsernameResponse = NoData;
export type UpdateUsernameResponse = NoData;
export type UpdateVariableDescriptionResponse = Variable;
@@ -5862,6 +5873,20 @@ export interface BatchRunProcedure {
export interface BeginPasskeyEnrollment {
}
/**
* Begin linking flow for a third party login. Response: [NoData].
*
* First call this method when authenticated, then
* redirect user to /api/auth/{provider}/link.
*
* 'provider' can be:
* - github
* - google
* - oidc
*/
export interface BeginThirdPartyLoginLink {
}
/**
* Starts enrollment flow for TOTP 2FA auth support.
* Response: [BeginTotpEnrollmentResponse]
@@ -9813,6 +9838,18 @@ export interface UnenrollPasskey {
export interface UnenrollTotp {
}
/** Unlink a login. Response: [NoData]. */
export interface UnlinkLogin {
/**
* 'provider' can be:
* - Local
* - Github
* - Google
* - Oidc
*/
provider: string;
}
/** Unpauses all containers on the target server. Response: [Update] */
export interface UnpauseAllContainers {
/** Name or id */
@@ -9989,6 +10026,16 @@ export interface UpdateOnboardingKey {
create_builder?: boolean;
}
/**
* Update the calling user's password. Response: [NoData].
*
* If the User was created using third party login method,
* using [UpdatePassword] adds or updates the Local linked (additional) login method.
*/
export interface UpdatePassword {
password: string;
}
/**
* **Admin only.** Update a user or user groups base permission level on a resource type.
* Response: [NoData].
@@ -10210,18 +10257,12 @@ export interface UpdateUserBasePermissions {
}
/**
* **Only for local users**. Update the calling users password.
* Update the calling users username.
* Response: [NoData].
*
* Will fail if the new username is invalid or already taken.
*/
export interface UpdateUserPassword {
password: string;
}
/**
* **Only for local users**. Update the calling users username.
* Response: [NoData].
*/
export interface UpdateUserUsername {
export interface UpdateUsername {
username: string;
}
@@ -10691,6 +10732,8 @@ export type UserIdOrTwoFactor =
export type UserRequest =
| { type: "PushRecentlyViewed", params: PushRecentlyViewed }
| { type: "SetLastSeenUpdate", params: SetLastSeenUpdate }
| { type: "UpdateUsername", params: UpdateUsername }
| { type: "UpdatePassword", params: UpdatePassword }
| { type: "CreateApiKey", params: CreateApiKey }
| { type: "DeleteApiKey", params: DeleteApiKey }
| { type: "BeginTotpEnrollment", params: BeginTotpEnrollment }
@@ -10698,7 +10741,9 @@ export type UserRequest =
| { type: "UnenrollTotp", params: UnenrollTotp }
| { type: "BeginPasskeyEnrollment", params: BeginPasskeyEnrollment }
| { type: "ConfirmPasskeyEnrollment", params: ConfirmPasskeyEnrollment }
| { type: "UnenrollPasskey", params: UnenrollPasskey };
| { type: "UnenrollPasskey", params: UnenrollPasskey }
| { type: "BeginThirdPartyLoginLink", params: BeginThirdPartyLoginLink }
| { type: "UnlinkLogin", params: UnlinkLogin };
export type WriteRequest =
| { type: "UpdateResourceMeta", params: UpdateResourceMeta }
@@ -10777,8 +10822,6 @@ export type WriteRequest =
| { type: "UpdateOnboardingKey", params: UpdateOnboardingKey }
| { type: "DeleteOnboardingKey", params: DeleteOnboardingKey }
| { type: "CreateLocalUser", params: CreateLocalUser }
| { type: "UpdateUserUsername", params: UpdateUserUsername }
| { type: "UpdateUserPassword", params: UpdateUserPassword }
| { type: "DeleteUser", params: DeleteUser }
| { type: "CreateServiceUser", params: CreateServiceUser }
| { type: "UpdateServiceUserDescription", params: UpdateServiceUserDescription }