kanidmd_lib/idm/
applinks.rs
1use crate::idm::server::IdmServerProxyReadTransaction;
2use crate::prelude::*;
3use kanidm_proto::internal::AppLink;
4
5impl IdmServerProxyReadTransaction<'_> {
6 pub fn list_applinks(&mut self, ident: &Identity) -> Result<Vec<AppLink>, OperationError> {
7 let Some(ident_mo) = ident.get_memberof() else {
9 debug!("Ident has no memberof, no applinks are present");
10 return Ok(Vec::with_capacity(0));
11 };
12
13 let f_executed = filter!(f_or(
21 ident_mo
22 .iter()
23 .copied()
24 .map(|uuid| { f_eq(Attribute::OAuth2RsScopeMap, PartialValue::Refer(uuid)) })
25 .collect()
26 ));
27 let f_intent = filter!(f_eq(
28 Attribute::Class,
29 EntryClass::OAuth2ResourceServer.into()
30 ));
31
32 let oauth2_related = self
34 .qs_read
35 .impersonate_search_ext(f_executed, f_intent, ident)?;
36 trace!(?oauth2_related);
37
38 let apps = oauth2_related
40 .iter()
41 .filter_map(|entry| {
42 let display_name = entry
43 .get_ava_single_utf8(Attribute::DisplayName)
44 .map(str::to_string)?;
45
46 let redirect_url = entry
47 .get_ava_single_url(Attribute::OAuth2RsOriginLanding)
48 .cloned()?;
49
50 let name = entry
51 .get_ava_single_iname(Attribute::Name)
52 .map(str::to_string)?;
53
54 let has_image = entry.get_ava_single_image(Attribute::Image).is_some();
55
56 Some(AppLink::Oauth2 {
57 name,
58 display_name,
59 redirect_url,
60 has_image,
61 })
62 })
63 .collect::<Vec<_>>();
64
65 debug!("returned {} related apps", apps.len());
66 trace!(?apps);
67
68 Ok(apps)
69 }
70}
71
72#[cfg(test)]
73mod tests {
74 use crate::prelude::*;
75 use kanidm_proto::internal::AppLink;
76
77 #[idm_test]
78 async fn test_idm_applinks_list(idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed) {
79 let ct = duration_from_epoch_now();
80 let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
81
82 let usr_uuid = Uuid::new_v4();
84 let grp_uuid = Uuid::new_v4();
85
86 let e_rs: Entry<EntryInit, EntryNew> = entry_init!(
87 (Attribute::Class, EntryClass::Object.to_value()),
88 (Attribute::Class, EntryClass::Account.to_value()),
89 (
90 Attribute::Class,
91 EntryClass::OAuth2ResourceServer.to_value()
92 ),
93 (
94 Attribute::Class,
95 EntryClass::OAuth2ResourceServerBasic.to_value()
96 ),
97 (Attribute::Name, Value::new_iname("test_resource_server")),
98 (
99 Attribute::DisplayName,
100 Value::new_utf8s("test_resource_server")
101 ),
102 (
103 Attribute::OAuth2RsOrigin,
104 Value::new_url_s("https://demo.example.com").unwrap()
105 ),
106 (
107 Attribute::OAuth2RsOriginLanding,
108 Value::new_url_s("https://demo.example.com/landing").unwrap()
109 ),
110 (
112 Attribute::OAuth2RsScopeMap,
113 Value::new_oauthscopemap(
114 grp_uuid,
115 btreeset![kanidm_proto::constants::OAUTH2_SCOPE_READ.to_string()]
116 )
117 .expect("invalid oauthscope")
118 )
119 );
120
121 let e_usr = entry_init!(
122 (Attribute::Class, EntryClass::Object.to_value()),
123 (Attribute::Class, EntryClass::Account.to_value()),
124 (Attribute::Class, EntryClass::Person.to_value()),
125 (Attribute::Name, Value::new_iname("testaccount")),
126 (Attribute::Uuid, Value::Uuid(usr_uuid)),
127 (Attribute::Description, Value::new_utf8s("testaccount")),
128 (Attribute::DisplayName, Value::new_utf8s("Test Account"))
129 );
130
131 let e_grp = entry_init!(
132 (Attribute::Class, EntryClass::Object.to_value()),
133 (Attribute::Class, EntryClass::Group.to_value()),
134 (Attribute::Uuid, Value::Uuid(grp_uuid)),
135 (Attribute::Name, Value::new_iname("test_oauth2_group"))
136 );
137
138 let ce = CreateEvent::new_internal(vec![e_rs, e_grp, e_usr]);
139 assert!(idms_prox_write.qs_write.create(&ce).is_ok());
140 assert!(idms_prox_write.commit().is_ok());
141
142 let mut idms_prox_read = idms.proxy_read().await.unwrap();
144
145 let ident = idms_prox_read
146 .qs_read
147 .internal_search_uuid(usr_uuid)
148 .map(Identity::from_impersonate_entry_readonly)
149 .expect("Failed to impersonate identity");
150
151 let apps = idms_prox_read
152 .list_applinks(&ident)
153 .expect("Failed to access related apps");
154
155 assert!(apps.is_empty());
156 drop(idms_prox_read);
157
158 let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
160 let me_inv_m = ModifyEvent::new_internal_invalid(
161 filter!(f_eq(Attribute::Uuid, PartialValue::Refer(grp_uuid))),
162 ModifyList::new_append(Attribute::Member, Value::Refer(usr_uuid)),
163 );
164 assert!(idms_prox_write.qs_write.modify(&me_inv_m).is_ok());
165 assert!(idms_prox_write.commit().is_ok());
166
167 let mut idms_prox_read = idms.proxy_read().await.unwrap();
168
169 let ident = idms_prox_read
170 .qs_read
171 .internal_search_uuid(usr_uuid)
172 .map(Identity::from_impersonate_entry_readonly)
173 .expect("Failed to impersonate identity");
174
175 let apps = idms_prox_read
176 .list_applinks(&ident)
177 .expect("Failed to access related apps");
178
179 let app = apps.first().expect("No apps return!");
180
181 assert!(match app {
182 AppLink::Oauth2 {
183 name,
184 display_name,
185 redirect_url,
186 has_image,
187 } => {
188 name == "test_resource_server"
189 && display_name == "test_resource_server"
190 && redirect_url
191 == &Url::parse("https://demo.example.com/landing")
192 .expect("Failed to parse URL")
193 && !has_image
194 } })
196 }
197}