identity_credential/credential/
linked_domain_service.rs1use identity_core::common::Object;
5use identity_core::common::OrderedSet;
6use identity_core::common::Url;
7use identity_did::DIDUrl;
8use identity_document::service::Service;
9use identity_document::service::ServiceBuilder;
10use identity_document::service::ServiceEndpoint;
11use indexmap::map::IndexMap;
12
13use crate::error::Result;
14use crate::utils::url_only_includes_origin;
15use crate::Error;
16use crate::Error::DomainLinkageError;
17
18#[derive(Debug, Clone)]
20pub struct LinkedDomainService {
21 service: Service,
22}
23
24impl TryFrom<Service> for LinkedDomainService {
25 type Error = Error;
26
27 fn try_from(service: Service) -> std::result::Result<Self, Self::Error> {
28 LinkedDomainService::check_structure(&service)?;
29 Ok(LinkedDomainService { service })
30 }
31}
32
33impl From<LinkedDomainService> for Service {
34 fn from(service: LinkedDomainService) -> Self {
35 service.service
36 }
37}
38
39impl LinkedDomainService {
40 pub(crate) fn domain_linkage_service_type() -> &'static str {
41 "LinkedDomains"
42 }
43
44 pub fn new(did_url: DIDUrl, domains: impl Into<OrderedSet<Url>>, properties: Object) -> Result<Self> {
47 let domains: OrderedSet<Url> = domains.into();
48 for domain in domains.iter() {
49 if domain.scheme() != "https" {
50 return Err(DomainLinkageError("domain does not include `https` scheme".into()));
51 }
52 }
53 let builder: ServiceBuilder = Service::builder(properties)
54 .id(did_url)
55 .type_(Self::domain_linkage_service_type());
56 if domains.len() == 1 {
57 Ok(Self {
58 service: builder
59 .service_endpoint(ServiceEndpoint::One(
60 domains.into_iter().next().expect("the len should be 1"),
61 ))
62 .build()
63 .map_err(|err| DomainLinkageError(Box::new(err)))?,
64 })
65 } else {
66 let mut map: IndexMap<String, OrderedSet<Url>> = IndexMap::new();
67 map.insert("origins".to_owned(), domains);
68 let service = builder
69 .service_endpoint(ServiceEndpoint::Map(map))
70 .build()
71 .map_err(|err| DomainLinkageError(Box::new(err)))?;
72 Ok(Self { service })
73 }
74 }
75
76 pub fn check_structure(service: &Service) -> Result<()> {
81 if service.type_().len() != 1 {
82 return Err(DomainLinkageError("invalid service type".into()));
83 }
84
85 let service_type = service
86 .type_()
87 .get(0)
88 .ok_or_else(|| DomainLinkageError("missing service type".into()))?;
89
90 if service_type != Self::domain_linkage_service_type() {
91 return Err(DomainLinkageError(
92 format!("expected `{}` service type", Self::domain_linkage_service_type()).into(),
93 ));
94 }
95
96 match service.service_endpoint() {
97 ServiceEndpoint::One(endpoint) => {
98 if endpoint.scheme() != "https" {
99 Err(DomainLinkageError("domain does not include `https` scheme".into()))?;
100 }
101 if !url_only_includes_origin(endpoint) {
102 Err(DomainLinkageError(
103 "domain must not contain any path, query or fragment".into(),
104 ))?;
105 }
106 Ok(())
107 }
108 ServiceEndpoint::Set(_) => Err(DomainLinkageError(
109 "service endpoints must be either a string or an object containing an `origins` property".into(),
110 )),
111 ServiceEndpoint::Map(endpoint) => {
112 if endpoint.is_empty() {
113 return Err(DomainLinkageError("empty service endpoint map".into()));
114 }
115 let origins: &OrderedSet<Url> = endpoint
116 .get("origins")
117 .ok_or_else(|| DomainLinkageError("missing `origins` property in service endpoint".into()))?;
118
119 for origin in origins.iter() {
120 if origin.scheme() != "https" {
121 return Err(DomainLinkageError("domain does not include `https` scheme".into()));
122 }
123 if !url_only_includes_origin(origin) {
124 Err(DomainLinkageError(
125 "domain must not contain any path, query or fragment".into(),
126 ))?;
127 }
128 }
129 Ok(())
130 }
131 }
132 }
133
134 pub fn domains(&self) -> &[Url] {
136 match self.service.service_endpoint() {
137 ServiceEndpoint::One(endpoint) => std::slice::from_ref(endpoint),
138 ServiceEndpoint::Set(_) => {
139 unreachable!("the service endpoint is never a set per the `LinkedDomainService` type invariant")
140 }
141 ServiceEndpoint::Map(endpoint) => endpoint
142 .get("origins")
143 .expect("the `origins` property exists per the `LinkedDomainService` type invariant")
144 .as_slice(),
145 }
146 }
147
148 pub fn id(&self) -> &DIDUrl {
150 self.service.id()
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use crate::credential::linked_domain_service::LinkedDomainService;
157 use identity_core::common::Object;
158 use identity_core::common::OrderedSet;
159 use identity_core::common::Url;
160 use identity_core::convert::FromJson;
161 use identity_did::DIDUrl;
162 use identity_document::service::Service;
163 use serde_json::json;
164
165 #[test]
166 fn test_create_service_multiple_origins() {
167 let domain_1 = "https://foo.example-1.com";
168 let domain_2 = "https://bar.example-2.com";
169 let mut domains = OrderedSet::new();
170 domains.append(Url::parse(domain_1).unwrap());
171 domains.append(Url::parse(domain_2).unwrap());
172
173 let service: LinkedDomainService =
174 LinkedDomainService::new(DIDUrl::parse("did:example:123#foo").unwrap(), domains, Object::new()).unwrap();
175
176 let service_from_json: Service = Service::from_json_value(json!({
177 "id":"did:example:123#foo",
178 "type": "LinkedDomains",
179 "serviceEndpoint": {
180 "origins": [domain_1, domain_2]
181 }
182 }))
183 .unwrap();
184 assert_eq!(Service::from(service), service_from_json);
185 }
186
187 #[test]
188 fn test_create_service_single_origin() {
189 let mut domains: OrderedSet<Url> = OrderedSet::new();
190 domains.append(Url::parse("https://foo.example-1.com").unwrap());
191
192 let service: LinkedDomainService =
193 LinkedDomainService::new(DIDUrl::parse("did:example:123#foo").unwrap(), domains, Object::new()).unwrap();
194
195 let service_from_json: Service = Service::from_json_value(json!({
196 "id":"did:example:123#foo",
197 "type": "LinkedDomains",
198 "serviceEndpoint": "https://foo.example-1.com"
199 }))
200 .unwrap();
201 assert_eq!(Service::from(service), service_from_json);
202 }
203
204 #[test]
205 fn test_valid_domains() {
206 let service_1: Service = Service::from_json_value(json!({
207 "id":"did:example:123#foo",
208 "type": "LinkedDomains",
209 "serviceEndpoint": "https://foo.example-1.com"
210 }))
211 .unwrap();
212 let service_1: LinkedDomainService = LinkedDomainService::try_from(service_1).unwrap();
213 let domain: Vec<Url> = vec![Url::parse("https://foo.example-1.com").unwrap()];
214 assert_eq!(service_1.domains(), domain);
215
216 let service_2: Service = Service::from_json_value(json!({
217 "id":"did:example:123#foo",
218 "type": "LinkedDomains",
219 "serviceEndpoint": { "origins" : ["https://foo.example-1.com", "https://foo.example-2.com"]}
220 }))
221 .unwrap();
222 let service_2: LinkedDomainService = LinkedDomainService::try_from(service_2).unwrap();
223 let domains: Vec<Url> = vec![
224 Url::parse("https://foo.example-1.com").unwrap(),
225 Url::parse("https://foo.example-2.com").unwrap(),
226 ];
227 assert_eq!(service_2.domains(), domains);
228 }
229
230 #[test]
231 fn test_extract_domains_invalid_scheme() {
232 let service_1: Service = Service::from_json_value(json!({
234 "id":"did:example:123#foo",
235 "type": "LinkedDomains",
236 "serviceEndpoint": "http://foo.example-1.com"
237 }))
238 .unwrap();
239 assert!(LinkedDomainService::try_from(service_1).is_err());
240
241 let service_2: Service = Service::from_json_value(json!({
242 "id":"did:example:123#foo",
243 "type": "LinkedDomains",
244 "serviceEndpoint": { "origins" : ["https://foo.example-1.com", "http://foo.example-2.com"]}
245 }))
246 .unwrap();
247 assert!(LinkedDomainService::try_from(service_2).is_err());
248 }
249
250 #[test]
251 fn test_extract_domain_type_check() {
252 let service_1: Service = Service::from_json_value(json!({
254 "id":"did:example:123#foo",
255 "type": "LinkedDomains",
256 "serviceEndpoint": "https://foo.example-1.com"
257 }))
258 .unwrap();
259 assert!(LinkedDomainService::try_from(service_1).is_ok());
260
261 let service_2: Service = Service::from_json_value(json!({
263 "id":"did:example:123#foo",
264 "type": "LinkedDomain",
265 "serviceEndpoint": "https://foo.example-1.com"
266 }))
267 .unwrap();
268 assert!(LinkedDomainService::try_from(service_2).is_err());
269 }
270}