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        // From the member-of of the ident.
8        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        // Formerly we did an internal search here, but we no longer need to since we have
14        // the access control module setup so that we can search for and see rs that we
15        // have access to.
16        //
17        // We do this weird looking f_executed/f_intent shenanigans to actually search
18        // on what we have access to, but we apply access as though we did a search on
19        // class=oauth2_resource_server instead, and we still apply access here.
20        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        // _ext reduces the entries based on access.
33        let oauth2_related = self
34            .qs_read
35            .impersonate_search_ext(f_executed, f_intent, ident)?;
36        trace!(?oauth2_related);
37
38        // Aggregate results to a Vec of AppLink
39        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        // Create an RS, the user and a group..
83        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            // System admins
111            (
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        // Now do an applink query, they will not be there.
143        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        // Add them to the group.
159        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            } // _ => false,
195        })
196    }
197}