• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Unleash / unleash-edge / 15441274495

04 Jun 2025 11:37AM UTC coverage: 78.265% (+10.3%) from 67.995%
15441274495

push

github

web-flow
task(rust): Update Rust version to 1.87.0 (#970)

10140 of 12956 relevant lines covered (78.26%)

158.36 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.28
/server/src/auth/token_validator.rs
1
use std::sync::Arc;
2

3
use dashmap::DashMap;
4
use tracing::trace;
5
use unleash_types::Upsert;
6

7
use crate::http::refresher::feature_refresher::FeatureRefresher;
8
use crate::http::unleash_client::UnleashClient;
9
use crate::persistence::EdgePersistence;
10
use crate::types::{
11
    EdgeResult, EdgeToken, TokenType, TokenValidationStatus, ValidateTokensRequest,
12
};
13

14
#[derive(Clone)]
15
pub struct TokenValidator {
16
    pub unleash_client: Arc<UnleashClient>,
17
    pub token_cache: Arc<DashMap<String, EdgeToken>>,
18
    pub persistence: Option<Arc<dyn EdgePersistence>>,
19
}
20

21
pub(crate) trait TokenRegister {
22
    async fn register_token(&self, token: String) -> EdgeResult<EdgeToken>;
23
}
24

25
impl TokenRegister for TokenValidator {
26
    async fn register_token(&self, token: String) -> EdgeResult<EdgeToken> {
25✔
27
        Ok(self
25✔
28
            .register_tokens(vec![token])
25✔
29
            .await?
25✔
30
            .first()
25✔
31
            .expect("Couldn't validate token")
25✔
32
            .clone())
25✔
33
    }
25✔
34
}
35

36
impl TokenValidator {
37
    async fn get_unknown_and_known_tokens(
38✔
38
        &self,
38✔
39
        tokens: Vec<String>,
38✔
40
    ) -> (Vec<EdgeToken>, Vec<EdgeToken>) {
38✔
41
        let tokens_with_valid_format: Vec<EdgeToken> = tokens
38✔
42
            .into_iter()
38✔
43
            .filter_map(|t| EdgeToken::try_from(t).ok())
42✔
44
            .collect();
38✔
45

38✔
46
        if tokens_with_valid_format.is_empty() {
38✔
47
            (vec![], vec![])
1✔
48
        } else {
49
            let mut tokens: Vec<EdgeToken> = vec![];
37✔
50
            for token in tokens_with_valid_format {
77✔
51
                let owned_token = self
40✔
52
                    .token_cache
40✔
53
                    .get(&token.token.clone())
40✔
54
                    .map(|t| t.value().clone())
40✔
55
                    .unwrap_or_else(|| token.clone());
40✔
56
                tokens.push(owned_token);
40✔
57
            }
40✔
58
            tokens.into_iter().partition(|t| t.token_type.is_none())
40✔
59
        }
60
    }
38✔
61

62
    pub async fn register_tokens(&self, tokens: Vec<String>) -> EdgeResult<Vec<EdgeToken>> {
38✔
63
        let (unknown_tokens, known_tokens) = self.get_unknown_and_known_tokens(tokens).await;
38✔
64
        if unknown_tokens.is_empty() {
38✔
65
            Ok(known_tokens)
30✔
66
        } else {
67
            let token_strings_to_validate: Vec<String> =
8✔
68
                unknown_tokens.iter().map(|t| t.token.clone()).collect();
9✔
69

70
            let validation_result = self
8✔
71
                .unleash_client
8✔
72
                .validate_tokens(ValidateTokensRequest {
8✔
73
                    tokens: token_strings_to_validate,
8✔
74
                })
8✔
75
                .await?;
8✔
76
            let tokens_to_sink: Vec<EdgeToken> = unknown_tokens
8✔
77
                .into_iter()
8✔
78
                .map(|maybe_valid| {
9✔
79
                    if let Some(validated_token) = validation_result
9✔
80
                        .iter()
9✔
81
                        .find(|v| maybe_valid.token == v.token)
9✔
82
                    {
83
                        trace!("Validated token");
8✔
84
                        EdgeToken {
8✔
85
                            status: TokenValidationStatus::Validated,
8✔
86
                            ..validated_token.clone()
8✔
87
                        }
8✔
88
                    } else {
89
                        trace!("Invalid token");
1✔
90
                        EdgeToken {
1✔
91
                            status: TokenValidationStatus::Invalid,
1✔
92
                            token_type: Some(TokenType::Invalid),
1✔
93
                            ..maybe_valid
1✔
94
                        }
1✔
95
                    }
96
                })
9✔
97
                .collect();
8✔
98
            tokens_to_sink.iter().for_each(|t| {
9✔
99
                self.token_cache.insert(t.token.clone(), t.clone());
9✔
100
            });
9✔
101
            let updated_tokens = tokens_to_sink.upsert(known_tokens);
8✔
102
            if let Some(persist) = self.persistence.clone() {
8✔
103
                let _ = persist.save_tokens(updated_tokens.clone()).await;
×
104
            }
8✔
105
            Ok(updated_tokens)
8✔
106
        }
107
    }
38✔
108

109
    pub async fn schedule_validation_of_known_tokens(&self, validation_interval_seconds: u64) {
×
110
        let sleep_duration = tokio::time::Duration::from_secs(validation_interval_seconds);
×
111
        loop {
112
            tokio::select! {
×
113
                _ = tokio::time::sleep(sleep_duration) => {
×
114
                    let _ = self.revalidate_known_tokens().await;
×
115
                }
116
            }
117
        }
118
    }
119

120
    pub async fn schedule_revalidation_of_startup_tokens(
×
121
        &self,
×
122
        tokens: Vec<String>,
×
123
        refresher: Option<Arc<FeatureRefresher>>,
×
124
    ) {
×
125
        let sleep_duration = tokio::time::Duration::from_secs(1);
×
126
        loop {
127
            tokio::select! {
×
128
                _ = tokio::time::sleep(sleep_duration) => {
×
129
                    if let Some(refresher) = refresher.clone() {
×
130
                        let token_result = self.register_tokens(tokens.clone()).await;
×
131
                        if let Ok(good_tokens) = token_result {
×
132
                            for token in good_tokens {
×
133
                                let _ = refresher.register_and_hydrate_token(&token).await;
×
134
                            }
135
                        }
×
136
                    }
×
137
                }
138
            }
139
        }
140
    }
141

142
    pub async fn revalidate_known_tokens(&self) -> EdgeResult<()> {
2✔
143
        let tokens_to_validate: Vec<String> = self
2✔
144
            .token_cache
2✔
145
            .iter()
2✔
146
            .filter(|t| t.value().status == TokenValidationStatus::Validated)
3✔
147
            .map(|e| e.key().clone())
3✔
148
            .collect();
2✔
149
        if !tokens_to_validate.is_empty() {
2✔
150
            let validation_result = self
2✔
151
                .unleash_client
2✔
152
                .validate_tokens(ValidateTokensRequest {
2✔
153
                    tokens: tokens_to_validate.clone(),
2✔
154
                })
2✔
155
                .await;
2✔
156

157
            if let Ok(valid_tokens) = validation_result {
2✔
158
                let invalid = tokens_to_validate
2✔
159
                    .into_iter()
2✔
160
                    .filter(|t| !valid_tokens.iter().any(|e| &e.token == t));
3✔
161
                for token in invalid {
3✔
162
                    self.token_cache
1✔
163
                        .entry(token)
1✔
164
                        .and_modify(|t| t.status = TokenValidationStatus::Invalid);
1✔
165
                }
1✔
166
            }
×
167
        }
×
168
        Ok(())
2✔
169
    }
2✔
170
}
171

172
#[cfg(test)]
173
mod tests {
174
    use std::sync::Arc;
175

176
    use actix_http::HttpService;
177
    use actix_http_test::{TestServer, test_server};
178
    use actix_service::map_config;
179
    use actix_web::{App, HttpResponse, dev::AppConfig, web};
180
    use dashmap::DashMap;
181
    use serde::{Deserialize, Serialize};
182

183
    use crate::{
184
        http::unleash_client::UnleashClient,
185
        types::{EdgeToken, TokenType, TokenValidationStatus},
186
    };
187

188
    use super::TokenValidator;
189

190
    #[derive(Clone, Debug, Serialize, Deserialize)]
191
    pub struct EdgeTokens {
192
        pub tokens: Vec<EdgeToken>,
193
    }
194

195
    async fn return_validated_tokens() -> HttpResponse {
1✔
196
        HttpResponse::Ok().json(EdgeTokens {
1✔
197
            tokens: valid_tokens(),
1✔
198
        })
1✔
199
    }
1✔
200

201
    fn valid_tokens() -> Vec<EdgeToken> {
1✔
202
        vec![EdgeToken {
1✔
203
            token: "*:development.1d38eefdd7bf72676122b008dcf330f2f2aa2f3031438e1b7e8f0d1f".into(),
1✔
204
            projects: vec!["*".into()],
1✔
205
            environment: Some("development".into()),
1✔
206
            token_type: Some(TokenType::Client),
1✔
207
            status: TokenValidationStatus::Validated,
1✔
208
        }]
1✔
209
    }
1✔
210

211
    async fn test_validation_server() -> TestServer {
2✔
212
        test_server(move || {
2✔
213
            HttpService::new(map_config(
2✔
214
                App::new().service(
2✔
215
                    web::resource("/edge/validate").route(web::post().to(return_validated_tokens)),
2✔
216
                ),
2✔
217
                |_| AppConfig::default(),
2✔
218
            ))
2✔
219
            .tcp()
2✔
220
        })
2✔
221
        .await
2✔
222
    }
2✔
223

224
    async fn validation_server_with_valid_tokens(
2✔
225
        token_cache: Arc<DashMap<String, EdgeToken>>,
2✔
226
    ) -> TestServer {
2✔
227
        let token_cache_wrapper = web::Data::from(token_cache.clone());
2✔
228
        let token_validator = web::Data::new(TokenValidator {
2✔
229
            token_cache: token_cache.clone(),
2✔
230
            persistence: None,
2✔
231
            unleash_client: Arc::new(UnleashClient::new("http://localhost:4242", None).unwrap()),
2✔
232
        });
2✔
233
        test_server(move || {
2✔
234
            HttpService::new(map_config(
2✔
235
                App::new()
2✔
236
                    .app_data(token_cache_wrapper.clone())
2✔
237
                    .app_data(token_validator.clone())
2✔
238
                    .service(web::scope("/edge").service(crate::edge_api::validate)),
2✔
239
                |_| AppConfig::default(),
2✔
240
            ))
2✔
241
            .tcp()
2✔
242
        })
2✔
243
        .await
2✔
244
    }
2✔
245

246
    #[tokio::test]
247
    pub async fn can_validate_tokens() {
1✔
248
        let srv = test_validation_server().await;
1✔
249
        let unleash_client =
1✔
250
            crate::http::unleash_client::UnleashClient::new(srv.url("/").as_str(), None)
1✔
251
                .expect("Couldn't build client");
1✔
252
        let validation_holder = TokenValidator {
1✔
253
            unleash_client: Arc::new(unleash_client),
1✔
254
            token_cache: Arc::new(DashMap::default()),
1✔
255
            persistence: None,
1✔
256
        };
1✔
257

1✔
258
        let tokens_to_validate = vec![
1✔
259
            "*:development.1d38eefdd7bf72676122b008dcf330f2f2aa2f3031438e1b7e8f0d1f".into(),
1✔
260
            "*:production.abcdef1234567890".into(),
1✔
261
        ];
1✔
262
        validation_holder
1✔
263
            .register_tokens(tokens_to_validate)
1✔
264
            .await
1✔
265
            .expect("Couldn't register tokens");
1✔
266
        assert_eq!(validation_holder.token_cache.len(), 2);
1✔
267
        assert!(validation_holder.token_cache.iter().any(|t| t.value().token
2✔
268
            == "*:development.1d38eefdd7bf72676122b008dcf330f2f2aa2f3031438e1b7e8f0d1f"
2✔
269
            && t.status == TokenValidationStatus::Validated));
1✔
270
        assert!(
1✔
271
            validation_holder
1✔
272
                .token_cache
1✔
273
                .iter()
1✔
274
                .any(|t| t.value().token == "*:production.abcdef1234567890"
1✔
275
                    && t.value().status == TokenValidationStatus::Invalid)
1✔
276
        );
1✔
277
    }
1✔
278

279
    #[tokio::test]
280
    pub async fn tokens_with_wrong_format_is_not_included() {
1✔
281
        let srv = test_validation_server().await;
1✔
282
        let unleash_client =
1✔
283
            UnleashClient::new(srv.url("/").as_str(), None).expect("Couldn't build client");
1✔
284
        let validation_holder = TokenValidator {
1✔
285
            unleash_client: Arc::new(unleash_client),
1✔
286
            token_cache: Arc::new(DashMap::default()),
1✔
287
            persistence: None,
1✔
288
        };
1✔
289
        let invalid_tokens = vec!["jamesbond".into(), "invalidtoken".into()];
1✔
290
        let validated_tokens = validation_holder
1✔
291
            .register_tokens(invalid_tokens)
1✔
292
            .await
1✔
293
            .unwrap();
1✔
294
        assert!(validated_tokens.is_empty());
1✔
295
    }
1✔
296

297
    #[tokio::test]
298
    pub async fn upstream_invalid_tokens_are_set_to_invalid() {
1✔
299
        let upstream_tokens = Arc::new(DashMap::default());
1✔
300
        let mut valid_token_development =
1✔
301
            EdgeToken::try_from("*:development.secret123".to_string()).expect("Bad Test Data");
1✔
302
        valid_token_development.status = TokenValidationStatus::Validated;
1✔
303
        valid_token_development.token_type = Some(TokenType::Client);
1✔
304
        upstream_tokens.insert(
1✔
305
            valid_token_development.token.clone(),
1✔
306
            valid_token_development.clone(),
1✔
307
        );
1✔
308
        let mut no_longer_valid_token = EdgeToken::try_from("*:production.123secret".to_string())
1✔
309
            .expect("Bad test production token");
1✔
310
        no_longer_valid_token.status = TokenValidationStatus::Invalid;
1✔
311
        no_longer_valid_token.token_type = Some(TokenType::Client);
1✔
312
        upstream_tokens.insert(
1✔
313
            no_longer_valid_token.token.clone(),
1✔
314
            no_longer_valid_token.clone(),
1✔
315
        );
1✔
316

1✔
317
        let srv = validation_server_with_valid_tokens(upstream_tokens).await;
1✔
318
        let unleash_client =
1✔
319
            crate::http::unleash_client::UnleashClient::new(srv.url("/").as_str(), None)
1✔
320
                .expect("Couldn't build client");
1✔
321

1✔
322
        let local_token_cache = Arc::new(DashMap::default());
1✔
323
        let mut previously_valid_token = no_longer_valid_token.clone();
1✔
324
        previously_valid_token.status = TokenValidationStatus::Validated;
1✔
325
        local_token_cache.insert(
1✔
326
            previously_valid_token.token.clone(),
1✔
327
            previously_valid_token.clone(),
1✔
328
        );
1✔
329
        let validation_holder = TokenValidator {
1✔
330
            unleash_client: Arc::new(unleash_client),
1✔
331
            token_cache: local_token_cache.clone(),
1✔
332
            persistence: None,
1✔
333
        };
1✔
334
        let _ = validation_holder.revalidate_known_tokens().await;
1✔
335
        assert!(
1✔
336
            validation_holder
1✔
337
                .token_cache
1✔
338
                .iter()
1✔
339
                .all(|t| t.value().status == TokenValidationStatus::Invalid)
1✔
340
        );
1✔
341
    }
1✔
342

343
    #[tokio::test]
344
    pub async fn still_valid_tokens_are_left_untouched() {
1✔
345
        let upstream_tokens: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
1✔
346
        let mut valid_token_development =
1✔
347
            EdgeToken::try_from("*:development.secret123".to_string()).expect("Bad Test Data");
1✔
348
        valid_token_development.status = TokenValidationStatus::Validated;
1✔
349
        valid_token_development.token_type = Some(TokenType::Client);
1✔
350
        let mut valid_token_production =
1✔
351
            EdgeToken::try_from("*:production.magic123".to_string()).expect("Bad Test Data");
1✔
352
        valid_token_production.status = TokenValidationStatus::Validated;
1✔
353
        valid_token_production.token_type = Some(TokenType::Frontend);
1✔
354
        upstream_tokens.insert(
1✔
355
            valid_token_development.token.clone(),
1✔
356
            valid_token_development.clone(),
1✔
357
        );
1✔
358
        upstream_tokens.insert(
1✔
359
            valid_token_production.token.clone(),
1✔
360
            valid_token_production.clone(),
1✔
361
        );
1✔
362
        let server = validation_server_with_valid_tokens(upstream_tokens).await;
1✔
363
        let client = UnleashClient::new(server.url("/").as_str(), None).unwrap();
1✔
364
        let local_tokens: DashMap<String, EdgeToken> = DashMap::default();
1✔
365
        local_tokens.insert(
1✔
366
            valid_token_development.token.clone(),
1✔
367
            valid_token_development,
1✔
368
        );
1✔
369
        local_tokens.insert(
1✔
370
            valid_token_production.token.clone(),
1✔
371
            valid_token_production.clone(),
1✔
372
        );
1✔
373
        let validator = TokenValidator {
1✔
374
            token_cache: Arc::new(local_tokens),
1✔
375
            unleash_client: Arc::new(client),
1✔
376
            persistence: None,
1✔
377
        };
1✔
378
        let _ = validator.revalidate_known_tokens().await;
1✔
379
        assert_eq!(validator.token_cache.len(), 2);
1✔
380
        assert!(
1✔
381
            validator
1✔
382
                .token_cache
1✔
383
                .iter()
1✔
384
                .all(|t| t.value().status == TokenValidationStatus::Validated)
2✔
385
        );
1✔
386
    }
1✔
387
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc