Skip to main content

rustauth_scim/
metadata.rs

1//! SCIM metadata resources.
2
3use 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}