1use serde::{Deserialize, Serialize};
4
5use crate::errors::ScimError;
6use crate::mappings::resource_url;
7
8pub const LIST_RESPONSE_SCHEMA: &str = "urn:ietf:params:scim:api:messages:2.0:ListResponse";
9pub const SCIM_BULK_MAX_OPERATIONS: usize = 1000;
10pub const SCIM_BULK_MAX_PAYLOAD_SIZE: usize = 1_048_576;
11pub const SCIM_FILTER_MAX_RESULTS: usize = 200;
12pub const SCIM_USER_SCHEMA_ID: &str = "urn:ietf:params:scim:schemas:core:2.0:User";
13pub const SCIM_GROUP_SCHEMA_ID: &str = "urn:ietf:params:scim:schemas:core:2.0:Group";
14pub const SCIM_ENTERPRISE_USER_SCHEMA_ID: &str =
15 "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User";
16const SCIM_SCHEMA_SCHEMA: &str = "urn:ietf:params:scim:schemas:core:2.0:Schema";
17const SCIM_RESOURCE_TYPE_SCHEMA: &str = "urn:ietf:params:scim:schemas:core:2.0:ResourceType";
18
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20pub struct ServiceProviderConfig {
21 pub patch: Supported,
22 pub bulk: Supported,
23 pub filter: Supported,
24 #[serde(rename = "changePassword")]
25 pub change_password: Supported,
26 pub sort: Supported,
27 pub etag: Supported,
28 #[serde(rename = "authenticationSchemes")]
29 pub authentication_schemes: Vec<AuthenticationScheme>,
30 pub schemas: Vec<String>,
31 pub meta: ServiceProviderMeta,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35pub struct Supported {
36 pub supported: bool,
37 #[serde(
38 rename = "maxOperations",
39 skip_serializing_if = "Option::is_none",
40 default
41 )]
42 pub max_operations: Option<usize>,
43 #[serde(
44 rename = "maxPayloadSize",
45 skip_serializing_if = "Option::is_none",
46 default
47 )]
48 pub max_payload_size: Option<usize>,
49 #[serde(
50 rename = "maxResults",
51 skip_serializing_if = "Option::is_none",
52 default
53 )]
54 pub max_results: Option<usize>,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58pub struct AuthenticationScheme {
59 pub name: String,
60 pub description: String,
61 #[serde(rename = "specUri")]
62 pub spec_uri: String,
63 #[serde(rename = "type")]
64 pub type_: String,
65 pub primary: bool,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
69pub struct ServiceProviderMeta {
70 #[serde(rename = "resourceType")]
71 pub resource_type: String,
72}
73
74#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
75pub struct ListResponse<T> {
76 pub schemas: Vec<String>,
77 #[serde(rename = "totalResults")]
78 pub total_results: usize,
79 #[serde(rename = "startIndex")]
80 pub start_index: usize,
81 #[serde(rename = "itemsPerPage")]
82 pub items_per_page: usize,
83 #[serde(rename = "Resources")]
84 pub resources: Vec<T>,
85}
86
87#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
88pub struct ScimSchema {
89 pub id: String,
90 pub schemas: Vec<String>,
91 pub name: String,
92 pub description: String,
93 pub attributes: Vec<ScimSchemaAttribute>,
94 pub meta: ScimMeta,
95}
96
97#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
98#[serde(rename_all = "camelCase")]
99pub struct ScimSchemaAttribute {
100 pub name: String,
101 #[serde(rename = "type")]
102 pub type_: String,
103 pub multi_valued: bool,
104 pub description: String,
105 pub required: bool,
106 #[serde(skip_serializing_if = "Option::is_none")]
107 pub case_exact: Option<bool>,
108 #[serde(skip_serializing_if = "Option::is_none")]
109 pub mutability: Option<String>,
110 #[serde(skip_serializing_if = "Option::is_none")]
111 pub returned: Option<String>,
112 #[serde(skip_serializing_if = "Option::is_none")]
113 pub uniqueness: Option<String>,
114 #[serde(skip_serializing_if = "Vec::is_empty", default)]
115 pub sub_attributes: Vec<ScimSchemaAttribute>,
116 #[serde(
117 rename = "referenceTypes",
118 skip_serializing_if = "Vec::is_empty",
119 default
120 )]
121 pub reference_types: Vec<String>,
122}
123
124#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
125#[serde(rename_all = "camelCase")]
126pub struct ScimMeta {
127 pub resource_type: String,
128 pub location: String,
129}
130
131#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
132pub struct ResourceType {
133 pub schemas: Vec<String>,
134 pub id: String,
135 pub name: String,
136 pub endpoint: String,
137 pub description: String,
138 pub schema: String,
139 #[serde(
140 rename = "schemaExtensions",
141 skip_serializing_if = "Vec::is_empty",
142 default
143 )]
144 pub schema_extensions: Vec<ResourceTypeSchemaExtension>,
145 pub meta: ScimMeta,
146}
147
148#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
149pub struct ResourceTypeSchemaExtension {
150 pub schema: String,
151 pub required: bool,
152}
153
154pub fn service_provider_config() -> ServiceProviderConfig {
155 ServiceProviderConfig {
156 patch: Supported::new(true),
157 bulk: Supported::new(true)
158 .max_operations(SCIM_BULK_MAX_OPERATIONS)
159 .max_payload_size(SCIM_BULK_MAX_PAYLOAD_SIZE),
160 filter: Supported::new(true).max_results(SCIM_FILTER_MAX_RESULTS),
161 change_password: Supported::new(false),
162 sort: Supported::new(true),
163 etag: Supported::new(true),
164 authentication_schemes: vec![AuthenticationScheme {
165 name: "OAuth Bearer Token".to_owned(),
166 description:
167 "Authentication scheme using the Authorization header with a bearer token tied to an organization."
168 .to_owned(),
169 spec_uri: "http://www.rfc-editor.org/info/rfc6750".to_owned(),
170 type_: "oauthbearertoken".to_owned(),
171 primary: true,
172 }],
173 schemas: vec!["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig".to_owned()],
174 meta: ServiceProviderMeta {
175 resource_type: "ServiceProviderConfig".to_owned(),
176 },
177 }
178}
179
180pub fn schemas(base_url: &str) -> ListResponse<ScimSchema> {
181 let resources = vec![
182 user_schema(base_url),
183 group_schema(base_url),
184 enterprise_user_schema(base_url),
185 ];
186 list_response(resources)
187}
188
189pub fn schema(base_url: &str, schema_id: &str) -> Result<ScimSchema, ScimError> {
190 match schema_id {
191 SCIM_USER_SCHEMA_ID => Ok(user_schema(base_url)),
192 SCIM_GROUP_SCHEMA_ID => Ok(group_schema(base_url)),
193 SCIM_ENTERPRISE_USER_SCHEMA_ID => Ok(enterprise_user_schema(base_url)),
194 _ => Err(ScimError::not_found("Schema not found")),
195 }
196}
197
198pub fn resource_types(base_url: &str) -> ListResponse<ResourceType> {
199 let resources = vec![user_resource_type(base_url), group_resource_type(base_url)];
200 list_response(resources)
201}
202
203pub fn resource_type(base_url: &str, resource_type_id: &str) -> Result<ResourceType, ScimError> {
204 match resource_type_id {
205 "User" => Ok(user_resource_type(base_url)),
206 "Group" => Ok(group_resource_type(base_url)),
207 _ => Err(ScimError::not_found("Resource type not found")),
208 }
209}
210
211fn list_response<T>(resources: Vec<T>) -> ListResponse<T> {
212 ListResponse {
213 schemas: vec![LIST_RESPONSE_SCHEMA.to_owned()],
214 total_results: resources.len(),
215 start_index: 1,
216 items_per_page: resources.len(),
217 resources,
218 }
219}
220
221fn user_schema(base_url: &str) -> ScimSchema {
222 ScimSchema {
223 id: SCIM_USER_SCHEMA_ID.to_owned(),
224 schemas: vec![SCIM_SCHEMA_SCHEMA.to_owned()],
225 name: "User".to_owned(),
226 description: "User Account".to_owned(),
227 attributes: user_attributes(),
228 meta: ScimMeta {
229 resource_type: "Schema".to_owned(),
230 location: resource_url(
231 base_url,
232 "/scim/v2/Schemas/urn:ietf:params:scim:schemas:core:2.0:User",
233 ),
234 },
235 }
236}
237
238fn group_schema(base_url: &str) -> ScimSchema {
239 ScimSchema {
240 id: SCIM_GROUP_SCHEMA_ID.to_owned(),
241 schemas: vec![SCIM_SCHEMA_SCHEMA.to_owned()],
242 name: "Group".to_owned(),
243 description: "Group".to_owned(),
244 attributes: group_attributes(),
245 meta: ScimMeta {
246 resource_type: "Schema".to_owned(),
247 location: resource_url(
248 base_url,
249 "/scim/v2/Schemas/urn:ietf:params:scim:schemas:core:2.0:Group",
250 ),
251 },
252 }
253}
254
255fn enterprise_user_schema(base_url: &str) -> ScimSchema {
256 ScimSchema {
257 id: SCIM_ENTERPRISE_USER_SCHEMA_ID.to_owned(),
258 schemas: vec![SCIM_SCHEMA_SCHEMA.to_owned()],
259 name: "EnterpriseUser".to_owned(),
260 description: "Enterprise User".to_owned(),
261 attributes: enterprise_user_attributes(),
262 meta: ScimMeta {
263 resource_type: "Schema".to_owned(),
264 location: resource_url(
265 base_url,
266 "/scim/v2/Schemas/urn:ietf:params:scim:schemas:extension:enterprise:2.0:User",
267 ),
268 },
269 }
270}
271
272fn user_resource_type(base_url: &str) -> ResourceType {
273 ResourceType {
274 schemas: vec![SCIM_RESOURCE_TYPE_SCHEMA.to_owned()],
275 id: "User".to_owned(),
276 name: "User".to_owned(),
277 endpoint: "/Users".to_owned(),
278 description: "User Account".to_owned(),
279 schema: SCIM_USER_SCHEMA_ID.to_owned(),
280 schema_extensions: vec![ResourceTypeSchemaExtension {
281 schema: SCIM_ENTERPRISE_USER_SCHEMA_ID.to_owned(),
282 required: false,
283 }],
284 meta: ScimMeta {
285 resource_type: "ResourceType".to_owned(),
286 location: resource_url(base_url, "/scim/v2/ResourceTypes/User"),
287 },
288 }
289}
290
291fn group_resource_type(base_url: &str) -> ResourceType {
292 ResourceType {
293 schemas: vec![SCIM_RESOURCE_TYPE_SCHEMA.to_owned()],
294 id: "Group".to_owned(),
295 name: "Group".to_owned(),
296 endpoint: "/Groups".to_owned(),
297 description: "Group".to_owned(),
298 schema: SCIM_GROUP_SCHEMA_ID.to_owned(),
299 schema_extensions: Vec::new(),
300 meta: ScimMeta {
301 resource_type: "ResourceType".to_owned(),
302 location: resource_url(base_url, "/scim/v2/ResourceTypes/Group"),
303 },
304 }
305}
306
307fn user_attributes() -> Vec<ScimSchemaAttribute> {
308 vec![
309 attr("id", "string", false, "Unique opaque identifier for the User", false)
310 .case_exact(true)
311 .read_only()
312 .uniqueness("server"),
313 attr(
314 "userName",
315 "string",
316 false,
317 "Unique identifier for the User, typically used by the user to directly authenticate to the service provider",
318 true,
319 )
320 .case_exact(false)
321 .read_write()
322 .uniqueness("server"),
323 attr(
324 "displayName",
325 "string",
326 false,
327 "The name of the User, suitable for display to end-users. The name SHOULD be the full name of the User being described, if known.",
328 false,
329 )
330 .case_exact(true)
331 .read_only()
332 .uniqueness("none"),
333 attr(
334 "active",
335 "boolean",
336 false,
337 "A Boolean value indicating the User's administrative status.",
338 false,
339 )
340 .read_only(),
341 attr("name", "complex", false, "The components of the user's real name.", false)
342 .sub_attributes(vec![
343 attr("formatted", "string", false, "The full name, including all middlenames, titles, and suffixes as appropriate, formatted for display(e.g., 'Ms. Barbara J Jensen, III').", false)
344 .case_exact(false)
345 .read_write()
346 .uniqueness("none"),
347 attr("familyName", "string", false, "The family name of the User, or last name in most Western languages (e.g., 'Jensen' given the fullname 'Ms. Barbara J Jensen, III').", false)
348 .case_exact(false)
349 .read_write()
350 .uniqueness("none"),
351 attr("givenName", "string", false, "The given name of the User, or first name in most Western languages (e.g., 'Barbara' given the full name 'Ms. Barbara J Jensen, III').", false)
352 .case_exact(false)
353 .read_write()
354 .uniqueness("none"),
355 ]),
356 attr("emails", "complex", true, "Email addresses for the user. The value SHOULD be canonicalized by the service provider, e.g., 'bjensen@example.com' instead of 'bjensen@EXAMPLE.COM'. Canonical type values of 'work', 'home', and 'other'.", false)
357 .read_write()
358 .uniqueness("none")
359 .sub_attributes(vec![
360 attr("value", "string", false, "Email addresses for the user. The value SHOULD be canonicalized by the service provider, e.g., 'bjensen@example.com' instead of 'bjensen@EXAMPLE.COM'. Canonical type values of 'work', 'home', and 'other'.", false)
361 .case_exact(false)
362 .read_write()
363 .uniqueness("server"),
364 attr("type", "string", false, "A label indicating the attribute's function.", false)
365 .case_exact(false)
366 .read_write()
367 .uniqueness("none"),
368 attr("display", "string", false, "A human-readable name, primarily used for display purposes.", false)
369 .case_exact(false)
370 .read_only()
371 .uniqueness("none"),
372 attr("primary", "boolean", false, "A Boolean value indicating the 'primary' or preferred attribute value for this attribute, e.g., the preferred mailing address or primary email address. The primary attribute value 'true' MUST appear no more than once.", false)
373 .read_write(),
374 ]),
375 simple_user_string("nickName", "The casual way to address the user in real life."),
376 attr("profileUrl", "reference", false, "A fully qualified URL pointing to a page representing the user's online profile.", false)
377 .case_exact(false)
378 .read_write()
379 .uniqueness("none")
380 .reference_types(vec!["external"]),
381 simple_user_string("title", "The user's title, such as Vice President."),
382 simple_user_string("userType", "Used to identify the relationship between the organization and the user."),
383 simple_user_string("preferredLanguage", "Indicates the user's preferred written or spoken language."),
384 simple_user_string("locale", "Used to indicate the user's default location for purposes of localizing items."),
385 simple_user_string("timezone", "The user's time zone in the Olson time zone database format."),
386 multi_valued_string("phoneNumbers", "Phone numbers for the user."),
387 multi_valued_string("ims", "Instant messaging addresses for the user."),
388 attr("photos", "complex", true, "URLs of photos of the user.", false)
389 .read_write()
390 .uniqueness("none")
391 .sub_attributes(multi_valued_common_sub_attributes("reference")),
392 attr("addresses", "complex", true, "A physical mailing address for the user.", false)
393 .read_write()
394 .uniqueness("none")
395 .sub_attributes(vec![
396 attr("formatted", "string", false, "The full mailing address, formatted for display or use with a mailing label.", false).case_exact(false).read_write().uniqueness("none"),
397 attr("streetAddress", "string", false, "The full street address component.", false).case_exact(false).read_write().uniqueness("none"),
398 attr("locality", "string", false, "The city or locality component.", false).case_exact(false).read_write().uniqueness("none"),
399 attr("region", "string", false, "The state or region component.", false).case_exact(false).read_write().uniqueness("none"),
400 attr("postalCode", "string", false, "The zip code or postal code component.", false).case_exact(false).read_write().uniqueness("none"),
401 attr("country", "string", false, "The country name component.", false).case_exact(false).read_write().uniqueness("none"),
402 attr("type", "string", false, "A label indicating the attribute's function.", false).case_exact(false).read_write().uniqueness("none"),
403 attr("primary", "boolean", false, "A Boolean value indicating the preferred attribute value.", false).read_write(),
404 ]),
405 attr("groups", "complex", true, "A list of groups to which the user belongs.", false)
406 .read_only()
407 .uniqueness("none")
408 .sub_attributes(vec![
409 attr("value", "string", false, "The identifier of the user's group.", false).case_exact(true).read_only().uniqueness("none"),
410 attr("$ref", "reference", false, "The URI of the corresponding Group resource.", false).case_exact(false).read_only().uniqueness("none").reference_types(vec!["Group"]),
411 attr("display", "string", false, "A human-readable name for the Group.", false).case_exact(false).read_only().uniqueness("none"),
412 ]),
413 multi_valued_string("entitlements", "A list of entitlements for the user."),
414 multi_valued_string("roles", "A list of roles for the user."),
415 attr("x509Certificates", "complex", true, "A list of certificates issued to the user.", false)
416 .read_write()
417 .uniqueness("none")
418 .sub_attributes(vec![
419 attr("value", "binary", false, "The value of an X.509 certificate.", false).case_exact(false).read_write().uniqueness("none"),
420 attr("type", "string", false, "A label indicating the attribute's function.", false).case_exact(false).read_write().uniqueness("none"),
421 attr("display", "string", false, "A human-readable name, primarily used for display purposes.", false).case_exact(false).read_only().uniqueness("none"),
422 attr("primary", "boolean", false, "A Boolean value indicating the preferred attribute value.", false).read_write(),
423 ]),
424 ]
425}
426
427fn group_attributes() -> Vec<ScimSchemaAttribute> {
428 vec![
429 attr(
430 "id",
431 "string",
432 false,
433 "Unique identifier for the Group",
434 false,
435 )
436 .case_exact(true)
437 .read_only()
438 .uniqueness("server"),
439 attr(
440 "displayName",
441 "string",
442 false,
443 "A human-readable name for the Group.",
444 true,
445 )
446 .case_exact(false)
447 .read_write()
448 .uniqueness("server"),
449 attr("members", "complex", true, "Members of the Group.", false)
450 .read_write()
451 .uniqueness("none")
452 .reference_types(vec!["User", "Group"])
453 .sub_attributes(vec![
454 attr("value", "string", false, "Identifier of the member.", false)
455 .case_exact(true)
456 .immutable()
457 .uniqueness("none"),
458 attr("$ref", "reference", false, "The URI of the member.", false)
459 .case_exact(false)
460 .read_only()
461 .uniqueness("none")
462 .reference_types(vec!["User", "Group"]),
463 attr(
464 "type",
465 "string",
466 false,
467 "The resource type of the member.",
468 false,
469 )
470 .case_exact(false)
471 .immutable()
472 .uniqueness("none"),
473 attr(
474 "display",
475 "string",
476 false,
477 "Display name of the member.",
478 false,
479 )
480 .case_exact(false)
481 .read_only()
482 .uniqueness("none"),
483 ]),
484 ]
485}
486
487fn enterprise_user_attributes() -> Vec<ScimSchemaAttribute> {
488 vec![
489 attr(
490 "employeeNumber",
491 "string",
492 false,
493 "Numeric or alphanumeric identifier assigned to a person.",
494 false,
495 )
496 .case_exact(false)
497 .read_write()
498 .uniqueness("none"),
499 attr(
500 "costCenter",
501 "string",
502 false,
503 "Identifies the name of a cost center.",
504 false,
505 )
506 .case_exact(false)
507 .read_write()
508 .uniqueness("none"),
509 attr(
510 "organization",
511 "string",
512 false,
513 "Identifies the name of an organization.",
514 false,
515 )
516 .case_exact(false)
517 .read_write()
518 .uniqueness("none"),
519 attr(
520 "division",
521 "string",
522 false,
523 "Identifies the name of a division.",
524 false,
525 )
526 .case_exact(false)
527 .read_write()
528 .uniqueness("none"),
529 attr(
530 "department",
531 "string",
532 false,
533 "Identifies the name of a department.",
534 false,
535 )
536 .case_exact(false)
537 .read_write()
538 .uniqueness("none"),
539 attr("manager", "complex", false, "The User's manager.", false)
540 .read_write()
541 .uniqueness("none")
542 .sub_attributes(vec![
543 attr(
544 "value",
545 "string",
546 false,
547 "The id of the SCIM resource representing the User's manager.",
548 false,
549 )
550 .case_exact(true)
551 .read_write()
552 .uniqueness("none"),
553 attr(
554 "$ref",
555 "reference",
556 false,
557 "The URI of the SCIM resource representing the User's manager.",
558 false,
559 )
560 .case_exact(false)
561 .read_write()
562 .uniqueness("none"),
563 attr(
564 "displayName",
565 "string",
566 false,
567 "The displayName of the User's manager.",
568 false,
569 )
570 .case_exact(false)
571 .read_write()
572 .uniqueness("none"),
573 ]),
574 ]
575}
576
577fn attr(
578 name: &str,
579 type_: &str,
580 multi_valued: bool,
581 description: &str,
582 required: bool,
583) -> ScimSchemaAttribute {
584 ScimSchemaAttribute {
585 name: name.to_owned(),
586 type_: type_.to_owned(),
587 multi_valued,
588 description: description.to_owned(),
589 required,
590 case_exact: None,
591 mutability: None,
592 returned: None,
593 uniqueness: None,
594 sub_attributes: Vec::new(),
595 reference_types: Vec::new(),
596 }
597}
598
599impl Supported {
600 fn new(supported: bool) -> Self {
601 Self {
602 supported,
603 max_operations: None,
604 max_payload_size: None,
605 max_results: None,
606 }
607 }
608
609 fn max_operations(mut self, value: usize) -> Self {
610 self.max_operations = Some(value);
611 self
612 }
613
614 fn max_payload_size(mut self, value: usize) -> Self {
615 self.max_payload_size = Some(value);
616 self
617 }
618
619 fn max_results(mut self, value: usize) -> Self {
620 self.max_results = Some(value);
621 self
622 }
623}
624
625impl ScimSchemaAttribute {
626 fn case_exact(mut self, value: bool) -> Self {
627 self.case_exact = Some(value);
628 self
629 }
630
631 fn read_only(mut self) -> Self {
632 self.mutability = Some("readOnly".to_owned());
633 self.returned = Some("default".to_owned());
634 self
635 }
636
637 fn read_write(mut self) -> Self {
638 self.mutability = Some("readWrite".to_owned());
639 self.returned = Some("default".to_owned());
640 self
641 }
642
643 fn immutable(mut self) -> Self {
644 self.mutability = Some("immutable".to_owned());
645 self.returned = Some("default".to_owned());
646 self
647 }
648
649 fn uniqueness(mut self, value: &str) -> Self {
650 self.uniqueness = Some(value.to_owned());
651 self
652 }
653
654 fn sub_attributes(mut self, sub_attributes: Vec<ScimSchemaAttribute>) -> Self {
655 self.sub_attributes = sub_attributes;
656 self
657 }
658
659 fn reference_types(mut self, reference_types: Vec<&str>) -> Self {
660 self.reference_types = reference_types.into_iter().map(str::to_owned).collect();
661 self
662 }
663}
664
665fn simple_user_string(name: &str, description: &str) -> ScimSchemaAttribute {
666 attr(name, "string", false, description, false)
667 .case_exact(false)
668 .read_write()
669 .uniqueness("none")
670}
671
672fn multi_valued_string(name: &str, description: &str) -> ScimSchemaAttribute {
673 attr(name, "complex", true, description, false)
674 .read_write()
675 .uniqueness("none")
676 .sub_attributes(multi_valued_common_sub_attributes("string"))
677}
678
679fn multi_valued_common_sub_attributes(value_type: &str) -> Vec<ScimSchemaAttribute> {
680 vec![
681 attr("value", value_type, false, "The attribute value.", false)
682 .case_exact(false)
683 .read_write()
684 .uniqueness("none"),
685 attr(
686 "type",
687 "string",
688 false,
689 "A label indicating the attribute's function.",
690 false,
691 )
692 .case_exact(false)
693 .read_write()
694 .uniqueness("none"),
695 attr(
696 "display",
697 "string",
698 false,
699 "A human-readable name, primarily used for display purposes.",
700 false,
701 )
702 .case_exact(false)
703 .read_only()
704 .uniqueness("none"),
705 attr(
706 "primary",
707 "boolean",
708 false,
709 "A Boolean value indicating the preferred attribute value.",
710 false,
711 )
712 .read_write(),
713 ]
714}