1mod api_key;
2mod device;
3mod jwk;
4mod models;
5mod passkey;
6mod two_factor;
7mod wallet;
8
9use crate::id::AdapterIdInput;
10use crate::store::schema::{EntityRole, resolve_field_name};
11use crate::user_fields::{AdapterRecord, UserConfig, UserFieldType};
12use crate::{AuthConfig, AuthError, AuthResult, FieldMap, FieldValue as Value, SchemaValue};
13use indexmap::{IndexMap, IndexSet};
14use std::sync::{Arc, LazyLock, Mutex};
15
16#[derive(Clone, Default)]
18pub struct ModelFields {
19 models: IndexMap<EntityRole, UserConfig>,
20 schema_models: Option<Vec<(EntityRole, &'static str)>>,
21 custom_models: IndexMap<String, (String, UserConfig)>,
22 user_plugin_fields: Vec<&'static str>,
23 session_plugin_fields: Vec<&'static str>,
24 native_fields: IndexMap<EntityRole, IndexSet<String>>,
25 organization_output_order: IndexMap<EntityRole, Vec<String>>,
26 organization: Option<crate::organization_fields::OrganizationFields>,
27 id_history: Arc<Mutex<IdHistory>>,
28}
29
30#[derive(Default)]
31struct IdHistory {
32 canonical: IndexSet<EntityRole>,
33 input: IndexMap<EntityRole, AdapterIdInput>,
34}
35
36fn optional_string(fields: &mut FieldMap, name: &str) -> Option<SchemaValue<Option<String>>> {
37 fields.shift_remove(name).map(SchemaValue::from_field)
38}
39
40impl ModelFields {
41 #[doc(hidden)]
43 pub fn fresh_runtime(&self) -> Self {
44 Self {
45 id_history: Arc::default(),
46 ..self.clone()
47 }
48 }
49
50 #[doc(hidden)]
52 pub fn canonicalize_id(&self, role: EntityRole) -> AuthResult<()> {
53 let _ = self
54 .id_history
55 .lock()
56 .map_err(|_| AuthError::internal("Adapter schema history lock poisoned"))?
57 .canonical
58 .insert(role);
59 Ok(())
60 }
61
62 #[doc(hidden)]
64 pub fn begin_id_input(&self, role: EntityRole, policy: AdapterIdInput) -> AuthResult<()> {
65 let mut history = self
66 .id_history
67 .lock()
68 .map_err(|_| AuthError::internal("Adapter schema history lock poisoned"))?;
69 let _ = history.canonical.insert(role);
70 let _ = history.input.insert(role, policy);
71 Ok(())
72 }
73
74 #[doc(hidden)]
76 pub fn begin_id_query(&self, role: EntityRole) -> AuthResult<()> {
77 self.begin_id_input(role, AdapterIdInput::default())
78 }
79
80 #[doc(hidden)]
82 pub fn begin_id_output(&self, role: EntityRole) -> AuthResult<()> {
83 let mut history = self
84 .id_history
85 .lock()
86 .map_err(|_| AuthError::internal("Adapter schema history lock poisoned"))?;
87 let _ = history.canonical.insert(role);
88 let _ = history.input.shift_remove(&role);
89 Ok(())
90 }
91
92 #[doc(hidden)]
94 pub fn id_input_active(&self, role: EntityRole) -> AuthResult<bool> {
95 Ok(self.id_input_policy(role)?.is_some())
96 }
97
98 #[doc(hidden)]
100 pub fn id_input_policy(&self, role: EntityRole) -> AuthResult<Option<AdapterIdInput>> {
101 Ok(self
102 .id_history
103 .lock()
104 .map_err(|_| AuthError::internal("Adapter schema history lock poisoned"))?
105 .input
106 .get(&role)
107 .copied())
108 }
109
110 pub(crate) fn runtime_fields(
111 &self,
112 role: EntityRole,
113 configured: &UserConfig,
114 ) -> AuthResult<UserConfig> {
115 let mut fields = configured.clone();
116 if self
117 .id_history
118 .lock()
119 .map_err(|_| AuthError::internal("Adapter schema history lock poisoned"))?
120 .canonical
121 .contains(&role)
122 {
123 let _ = fields.fields_mut().insert("id".into(), Default::default());
124 }
125 Ok(fields)
126 }
127
128 pub fn set_schema_configuration(&mut self, config: &crate::store::schema::SchemaConfiguration) {
130 self.schema_models = Some(config.models());
131 self.user_plugin_fields =
132 crate::wire::UserView::active_plugin_fields(&config.metadata).collect();
133 self.session_plugin_fields =
134 crate::wire::SessionView::active_plugin_fields(&config.metadata).collect();
135 }
136
137 #[doc(hidden)]
139 pub fn user_plugin_fields(&self) -> &[&'static str] {
140 &self.user_plugin_fields
141 }
142
143 #[doc(hidden)]
145 pub fn session_plugin_fields(&self) -> &[&'static str] {
146 &self.session_plugin_fields
147 }
148
149 pub(crate) fn organization_join_schema(&self, config: &AuthConfig) -> Self {
150 let mut schema = self.clone();
151 if schema.schema_models.is_none() {
152 schema.set_schema_configuration(&crate::store::schema::SchemaConfiguration {
154 config: Arc::new(config.clone()),
155 plugins: vec!["organization"],
156 metadata: [
157 (
158 "organization.teams_enabled".into(),
159 serde_json::Value::Bool(true),
160 ),
161 (
162 "organization.dynamic_access_control".into(),
163 serde_json::Value::Bool(true),
164 ),
165 ]
166 .into_iter()
167 .collect(),
168 secondary_storage: false,
169 database_rate_limit: false,
170 });
171 }
172 schema
173 }
174
175 pub(crate) fn register(&mut self, role: EntityRole, fields: UserConfig) -> AuthResult<()> {
176 match role {
177 EntityRole::ApiKey => {
178 let mut combined = self.fields(role).clone();
179 combined.fields_mut().extend(fields.fields().clone());
180 api_key::validate_fields(&combined)?;
181 }
182 EntityRole::DeviceCode => device::validate_fields(&fields)?,
183 EntityRole::Jwk => jwk::validate_fields(&fields)?,
184 EntityRole::Passkey => {
185 let mut combined = self.fields(role).clone();
186 combined.fields_mut().extend(fields.fields().clone());
187 passkey::validate_fields(&combined)?;
188 }
189 EntityRole::TwoFactor => two_factor::validate_fields(&fields)?,
190 EntityRole::WalletAddress => wallet::validate_fields(&fields)?,
191 EntityRole::User
192 | EntityRole::Session
193 | EntityRole::Account
194 | EntityRole::Verification
195 | EntityRole::Organization
196 | EntityRole::Team => {}
197 _ => {
198 return Err(AuthError::config(format!(
199 "Plugin field registration does not support {role:?}"
200 )));
201 }
202 }
203 self.extend(role, fields);
204 if role == EntityRole::Passkey
205 && let Some(fields) = self.models.get_mut(&role)
206 {
207 fields.fields_mut().sort_by(|left, _, right, _| {
209 passkey::field_order(left).cmp(&passkey::field_order(right))
210 });
211 }
212 if role == EntityRole::ApiKey
213 && let Some(fields) = self.models.get_mut(&role)
214 {
215 fields
216 .fields_mut()
217 .sort_by(|left, _, right, _| (left != "name").cmp(&(right != "name")));
218 }
219 Ok(())
220 }
221
222 pub(crate) fn extend(&mut self, role: EntityRole, fields: UserConfig) {
223 if let Some(fields) = fields.additional_fields {
224 if let Some(native) = self.native_fields.get_mut(&role) {
225 native.retain(|name| !fields.contains_key(name));
226 }
227 self.models
228 .entry(role)
229 .or_default()
230 .fields_mut()
231 .extend(fields);
232 }
233 }
234
235 pub(crate) fn register_organization_schema(
236 &mut self,
237 fields: &crate::organization_fields::OrganizationFields,
238 teams_enabled: bool,
239 ) {
240 self.organization = Some(fields.clone());
241 self.declare_native_fields(
242 EntityRole::Organization,
243 &["name", "slug", "logo", "createdAt", "metadata"],
244 fields.organization.clone(),
245 );
246 if teams_enabled {
247 self.declare_native_fields(
248 EntityRole::Team,
249 &[
250 "name",
251 "memberCount",
252 "organizationId",
253 "createdAt",
254 "updatedAt",
255 ],
256 fields.team.clone(),
257 );
258 }
259 self.declare_native_fields(
260 EntityRole::Member,
261 &["organizationId", "userId", "role", "createdAt"],
262 fields.member.clone(),
263 );
264 self.declare_native_fields(
265 EntityRole::Invitation,
266 &[
267 "organizationId",
268 "email",
269 "role",
270 "teamId",
271 "status",
272 "expiresAt",
273 "createdAt",
274 "inviterId",
275 ],
276 fields.invitation.clone(),
277 );
278 self.declare_native_fields(
279 EntityRole::OrganizationRole,
280 &[
281 "organizationId",
282 "role",
283 "permission",
284 "createdAt",
285 "updatedAt",
286 ],
287 fields.organization_role.clone(),
288 );
289 }
290
291 fn declare_native_fields(&mut self, role: EntityRole, names: &[&str], fields: UserConfig) {
292 let registered = self.models.entry(role).or_default().fields_mut();
293 let native = self.native_fields.entry(role).or_default();
294 for name in names {
295 let _ = registered.insert((*name).into(), Default::default());
297 let _ = native.insert((*name).into());
298 }
299 self.extend(role, fields);
300 }
301
302 pub fn organization_fields(
304 &self,
305 fields: crate::organization_fields::OrganizationFields,
306 ) -> crate::organization_fields::OrganizationFields {
307 let mut fields = self.organization.clone().unwrap_or(fields);
308 for (role, schema) in [
309 (EntityRole::Organization, &mut fields.organization),
310 (EntityRole::Team, &mut fields.team),
311 (EntityRole::Member, &mut fields.member),
312 (EntityRole::Invitation, &mut fields.invitation),
313 (EntityRole::OrganizationRole, &mut fields.organization_role),
314 ] {
315 let Some(registered) = self.models.get(&role) else {
316 continue;
317 };
318 let mut resolved = registered.clone();
319 for (name, field) in schema.fields() {
320 let _ = resolved
321 .fields_mut()
322 .entry(name.clone())
323 .or_insert_with(|| field.clone());
324 }
325 if let Some(native) = self.native_fields.get(&role) {
326 resolved
327 .fields_mut()
328 .retain(|name, _| !native.contains(name));
329 }
330 *schema = resolved;
331 }
332 for field in fields.organization_role.fields_mut().values_mut() {
334 field.required = Some(false);
335 }
336 fields
337 }
338
339 pub fn fields(&self, role: EntityRole) -> &UserConfig {
341 static EMPTY: LazyLock<UserConfig> = LazyLock::new(UserConfig::default);
342 self.models.get(&role).unwrap_or(&EMPTY)
343 }
344
345 pub(crate) fn iter(&self) -> impl Iterator<Item = (EntityRole, &UserConfig)> {
346 self.models.iter().map(|(role, fields)| (*role, fields))
347 }
348
349 pub(crate) fn organization_output_field_names(
350 &self,
351 role: EntityRole,
352 policies: &UserConfig,
353 ) -> Vec<String> {
354 let mut names = self
355 .organization_output_order
356 .get(&role)
357 .cloned()
358 .unwrap_or_else(|| {
359 let mut fields = Self::default();
360 fields.register_organization_schema(&Default::default(), true);
361 fields.fields(role).fields().keys().cloned().collect()
362 });
363 for name in policies.fields().keys() {
364 if !names.contains(name) {
365 names.push(name.clone());
366 }
367 }
368 names.retain(|name| name != "id");
369 names.push("id".into());
370 names
371 }
372
373 pub fn resolve(mut self, application: &AuthConfig) -> (AuthConfig, AuthConfig, Self) {
376 let mut adapter = application.clone();
377 let mut endpoint = application.clone();
378 let mut resolve = |role, configured| {
379 let fields = self.models.shift_remove(&role).unwrap_or_default();
380 super::resolve_user_fields(&configured, fields)
381 };
382 (adapter.user, endpoint.user) = resolve(EntityRole::User, application.user.clone());
383 let (stored, public) = resolve(EntityRole::Session, application.session.field_schema());
384 adapter.session_input_fields = Some(public.clone());
386 adapter.session.additional_fields = stored.additional_fields;
387 endpoint.session.additional_fields = public.additional_fields;
388 let (stored, public) = resolve(
389 EntityRole::Account,
390 UserConfig {
391 additional_fields: Some(application.account.additional_fields.clone()),
392 },
393 );
394 adapter.account.additional_fields = stored.additional_fields.unwrap_or_default();
395 endpoint.account.additional_fields = public.additional_fields.unwrap_or_default();
396 let (stored, public) = resolve(
397 EntityRole::Verification,
398 UserConfig {
399 additional_fields: Some(application.verification.additional_fields.clone()),
400 },
401 );
402 adapter.verification.additional_fields = stored.additional_fields.unwrap_or_default();
403 endpoint.verification.additional_fields = public.additional_fields.unwrap_or_default();
404 for (role, native) in &self.native_fields {
405 if let Some(fields) = self.models.get_mut(role) {
406 let _ = self
408 .organization_output_order
409 .insert(*role, fields.fields().keys().cloned().collect());
410 fields.fields_mut().retain(|name, _| !native.contains(name));
411 }
412 }
413 (adapter, endpoint, self)
414 }
415}