@@ -3,6 +3,8 @@ use std::fmt;
33
44use serde:: { Deserialize , Serialize } ;
55
6+ use crate :: global_settings:: LICENSE_KEY_SETTING ;
7+
68// ---------------------------------------------------------------------------
79// Kubernetes Secret reference support
810// ---------------------------------------------------------------------------
@@ -746,6 +748,33 @@ pub const PROTECTED_SETTINGS: &[&str] = &[
746748/// Internal settings that are never exposed via the API or included in config exports.
747749pub const HIDDEN_SETTINGS : & [ & str ] = & [ "uid" , "rsa_keys" , "jwt_secret" , "min_keep_alive_version" ] ;
748750
751+ /// Extract the expiry timestamp from a license key JSON value.
752+ ///
753+ /// License keys have the format `<client_id>.<expiry>.<signature>`.
754+ /// Returns `None` if the value is not a string or doesn't match the format.
755+ fn license_key_expiry ( value : & serde_json:: Value ) -> Option < u64 > {
756+ let s = value. as_str ( ) ?;
757+ let parts: Vec < & str > = s. split ( '.' ) . collect ( ) ;
758+ if parts. len ( ) != 3 {
759+ return None ;
760+ }
761+ parts[ 1 ] . parse :: < u64 > ( ) . ok ( )
762+ }
763+
764+ /// Returns true if two license key values share the same client ID and signature
765+ /// (i.e. they differ only in the expiry field).
766+ fn license_keys_same_except_expiry ( a : & serde_json:: Value , b : & serde_json:: Value ) -> bool {
767+ let ( Some ( a_str) , Some ( b_str) ) = ( a. as_str ( ) , b. as_str ( ) ) else {
768+ return false ;
769+ } ;
770+ let a_parts: Vec < & str > = a_str. split ( '.' ) . collect ( ) ;
771+ let b_parts: Vec < & str > = b_str. split ( '.' ) . collect ( ) ;
772+ if a_parts. len ( ) != 3 || b_parts. len ( ) != 3 {
773+ return false ;
774+ }
775+ a_parts[ 0 ] == b_parts[ 0 ] && a_parts[ 2 ] == b_parts[ 2 ]
776+ }
777+
749778/// Compute the diff between current and desired global settings.
750779pub fn diff_global_settings (
751780 current : & BTreeMap < String , serde_json:: Value > ,
@@ -756,6 +785,23 @@ pub fn diff_global_settings(
756785 for ( key, value) in desired {
757786 match current. get ( key) {
758787 Some ( existing) if existing == value => { } // no change
788+ Some ( existing) if key == LICENSE_KEY_SETTING => {
789+ if license_keys_same_except_expiry ( existing, value) {
790+ let current_expiry = license_key_expiry ( existing) . unwrap_or ( 0 ) ;
791+ let desired_expiry = license_key_expiry ( value) . unwrap_or ( 0 ) ;
792+ if desired_expiry > current_expiry {
793+ upserts. insert ( key. clone ( ) , value. clone ( ) ) ;
794+ } else {
795+ tracing:: info!(
796+ "Skipping license_key update: desired expiry ({}) is not posterior to current expiry ({})" ,
797+ desired_expiry,
798+ current_expiry
799+ ) ;
800+ }
801+ } else {
802+ upserts. insert ( key. clone ( ) , value. clone ( ) ) ;
803+ }
804+ }
759805 _ => {
760806 upserts. insert ( key. clone ( ) , value. clone ( ) ) ;
761807 }
@@ -1939,4 +1985,117 @@ mod tests {
19391985 Some ( "plain-token" )
19401986 ) ;
19411987 }
1988+
1989+ // -----------------------------------------------------------------------
1990+ // License key expiry diff tests
1991+ // -----------------------------------------------------------------------
1992+
1993+ #[ test]
1994+ fn diff_license_key_skips_older_expiry ( ) {
1995+ let mut current = BTreeMap :: new ( ) ;
1996+ current. insert (
1997+ "license_key" . to_string ( ) ,
1998+ serde_json:: json!( "client1.2000000000.sig123" ) ,
1999+ ) ;
2000+
2001+ let mut desired = BTreeMap :: new ( ) ;
2002+ desired. insert (
2003+ "license_key" . to_string ( ) ,
2004+ serde_json:: json!( "client1.1000000000.sig123" ) ,
2005+ ) ;
2006+
2007+ let diff = diff_global_settings ( & current, & desired, ApplyMode :: Merge ) ;
2008+ assert ! (
2009+ diff. upserts. is_empty( ) ,
2010+ "Should not update license_key when desired expiry is older"
2011+ ) ;
2012+ }
2013+
2014+ #[ test]
2015+ fn diff_license_key_skips_equal_expiry ( ) {
2016+ let mut current = BTreeMap :: new ( ) ;
2017+ current. insert (
2018+ "license_key" . to_string ( ) ,
2019+ serde_json:: json!( "client1.2000000000.sig_a" ) ,
2020+ ) ;
2021+
2022+ let mut desired = BTreeMap :: new ( ) ;
2023+ desired. insert (
2024+ "license_key" . to_string ( ) ,
2025+ serde_json:: json!( "client1.2000000000.sig_b" ) ,
2026+ ) ;
2027+
2028+ let diff = diff_global_settings ( & current, & desired, ApplyMode :: Merge ) ;
2029+ assert_eq ! (
2030+ diff. upserts. len( ) ,
2031+ 1 ,
2032+ "Different signature means different key, should update"
2033+ ) ;
2034+ }
2035+
2036+ #[ test]
2037+ fn diff_license_key_updates_newer_expiry ( ) {
2038+ let mut current = BTreeMap :: new ( ) ;
2039+ current. insert (
2040+ "license_key" . to_string ( ) ,
2041+ serde_json:: json!( "client1.1000000000.sig123" ) ,
2042+ ) ;
2043+
2044+ let mut desired = BTreeMap :: new ( ) ;
2045+ desired. insert (
2046+ "license_key" . to_string ( ) ,
2047+ serde_json:: json!( "client1.2000000000.sig123" ) ,
2048+ ) ;
2049+
2050+ let diff = diff_global_settings ( & current, & desired, ApplyMode :: Merge ) ;
2051+ assert_eq ! ( diff. upserts. len( ) , 1 ) ;
2052+ assert_eq ! (
2053+ diff. upserts[ "license_key" ] ,
2054+ serde_json:: json!( "client1.2000000000.sig123" )
2055+ ) ;
2056+ }
2057+
2058+ #[ test]
2059+ fn diff_license_key_updates_different_client ( ) {
2060+ let mut current = BTreeMap :: new ( ) ;
2061+ current. insert (
2062+ "license_key" . to_string ( ) ,
2063+ serde_json:: json!( "client1.2000000000.sig123" ) ,
2064+ ) ;
2065+
2066+ let mut desired = BTreeMap :: new ( ) ;
2067+ desired. insert (
2068+ "license_key" . to_string ( ) ,
2069+ serde_json:: json!( "client2.1000000000.sig456" ) ,
2070+ ) ;
2071+
2072+ let diff = diff_global_settings ( & current, & desired, ApplyMode :: Merge ) ;
2073+ assert_eq ! (
2074+ diff. upserts. len( ) ,
2075+ 1 ,
2076+ "Different client ID means different key, should always update"
2077+ ) ;
2078+ }
2079+
2080+ #[ test]
2081+ fn diff_license_key_non_string_always_updates ( ) {
2082+ let mut current = BTreeMap :: new ( ) ;
2083+ current. insert (
2084+ "license_key" . to_string ( ) ,
2085+ serde_json:: json!( { "envRef" : "LIC_KEY" } ) ,
2086+ ) ;
2087+
2088+ let mut desired = BTreeMap :: new ( ) ;
2089+ desired. insert (
2090+ "license_key" . to_string ( ) ,
2091+ serde_json:: json!( { "envRef" : "NEW_LIC_KEY" } ) ,
2092+ ) ;
2093+
2094+ let diff = diff_global_settings ( & current, & desired, ApplyMode :: Merge ) ;
2095+ assert_eq ! (
2096+ diff. upserts. len( ) ,
2097+ 1 ,
2098+ "Non-string license keys should always be updated when different"
2099+ ) ;
2100+ }
19422101}
0 commit comments