<commit_before>ruleset fuse_fleet_oauth {
    meta {
        name "Fuse Fleet OAuth"
        description <<
Ruleset for fleet OAuth stuff
        >>
	
	errors to b16x13

	use module b16x10 alias fuse_keys

        use module a169x625 alias CloudOS
        use module a169x676 alias pds
	use module b16x19 alias common
	use module b16x26 alias reports

	
	sharing on



	provides clientAccessToken,  refreshTokenForAccessToken, showTokens, forgetTokens, forgetAllTokens, tellOwner, // don't provide after debug
             isAuthorized, redirectUri, carvoyantOauthUrl, codeForAccessToken, getTokens, fixToken 

    }

    global {
    
      // duplicated here and in fuse_carvoyant.krl
      api_hostname = "api.carvoyant.com";
      apiHostname = function() {api_hostname};
      oauth_url = "https://"+api_hostname+"/oauth/token";

      // appears in both this ruleset and fuse_carvoyant
      namespace = function() {
        "fuse:carvoyant";
      };

      // ---------- authorization ----------

   
      // used for getting token to create an account; not for general use
      clientAccessToken = function() {
        header = 
            {"credentials": {
               "username": keys:carvoyant_client("client_id"),
               "password": keys:carvoyant_client("client_secret"),
	       "realm": apiHostname(),
	       "netloc": apiHostname() + ":443"
               },
             "params" : {"grant_type": "client_credentials"}
            }; //.klog(">>>>>> client header <<<<<<<<");
        raw_result = http:post(oauth_url, header);
        (raw_result{"status_code"} eq "200") => raw_result{"content"}.decode()
                                              | raw_result.decode()
      };

      redirectUri = function() {
        "https://" + meta:host() + "/sky/event/" + keys:anonymous_pico("eci")  + "/" + math:random(9999) +  "/oauth/new_oauth_code";
      } 



      // this function creates a carvoyant OAuth URL that leads the user to a login screen. 
      // the redirect URL gets picked up by the anonymous pico's CloudOS handleOauthCode rule 
      carvoyantOauthUrl = function(hostsite) {
    
        redirect_uri = redirectUri();
        accessing_eci = meta:eci();

        params = {"client_id" : keys:carvoyant_client("client_id"),	
                  "redirect_uri" : redirect_uri,
  		  "response_type" : "code",
  		  "state": [meta:rid(), accessing_eci, hostsite ].join(",")
		 };

        query_string = params.map(function(k,v){k+"="+v}).values().join("&").klog(">>>>> query string >>>>>>");
        {"url": "https://auth.carvoyant.com/OAuth/authorize?" + query_string}
      }



      // CloudOS:handleOauthCode rule redirects to this function based on state param created in carvoyantOathUrl()
      codeForAccessToken = function(code, redirect_uri, hostsite) {
        header = 
            {"credentials": {
               "username": keys:carvoyant_client("client_id"),
               "password": keys:car<PASSWORD>ant_client("client_secret"),
	       "realm": apiHostname(),
	       "netloc": apiHostname() + ":443"
               },
             "params" : {"grant_type": "authorization_code",
	                 "code": code,
			 "redirect_uri": redirect_uri
	                }
            }; //.klog(">>>>>> client header <<<<<<<<");
        raw_result = http:post(oauth_url, header);
        results = (raw_result{"status_code"} eq "200") => normalizeAccountInfo(raw_result{"content"}.decode())
                                                        | raw_result.decode();

        // hardcoded URL!!
        url = hostsite + "?" +
                results.map(function(k,v){k + "=" + v}).values().join("&").klog(">>>>> url >>>>>");
        
        page = <<
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title></title>
  <META HTTP-EQUIV="Refresh" CONTENT="0;url=#{url}">
  <meta name="robots" content="noindex"/>
  <link rel="canonical" href="#{url}"/>
</head>
<body>
<p>
You are being redirected to <a href="#{url}">#{url}</a>
</p>

</body>
</html>
        >>;

        page
      };

    fixToken = function() {
      account_info = ent:account_info || {};
      try_refresh = not account_info{"refresh_token"}.isnull();
      new_tokens = try_refresh => refreshTokenForAccessToken().klog(">>>>> refreshing for carvoyant_get() >>> ")
                                | {};
      new_tokens
    };


      refreshTokenForAccessToken = function() {
        account_info = ent:account_info || {};
        header = 
            {"credentials": {
               "username": keys:carvoyant_client("client_id"),
               "password": keys:car<PASSWORD>("client_secret"),
	       "realm": apiHostname(),
	       "netloc": apiHostname() + ":443"
               },
             "params" : {"grant_type": "refresh_token",
	                 "refresh_token": account_info{"refresh_token"}
	                }
            }; //.klog(">>>>>> client header <<<<<<<<");
        raw_result = http:post(oauth_url, header).klog(">>> refresh request result >>> ");
        invalid_grant = raw_result{"status_code"} eq "400" && 
	                raw_result{"content"}.decode().pick("$.error") eq "invalid_grant";
        (raw_result{"status_code"} eq "200") => normalizeAccountInfo(raw_result{"content"}.decode()) |
        invalid_grant                        => tellOwner()
                                              | raw_result.decode()
      };

      normalizeAccountInfo = function(account_info) {

        // raise an event to broadcast new config
//        response = cloudos:sendEvent(meta:eci(), "fuse", "config_outdated", account_info);
	
        // add the timestamp and then store the info in an entity var (ugh; evil)
	account_info{"access_token"}.isnull() => ent:account_info |
                                                 account_info.put(["timestamp"], time:now()).pset(ent:account_info);
      }

      forgetTokens = function(){
        "".pset(ent:account_info{["access_token"]})
      };

      forgetAllTokens = function(){
        {}.pset(ent:account_info)
      };

      showTokens = function() {
        ent:account_info
      };

      // only give tokens to pico who identify themselves and we confirm they came in on the channel
      //   that they are subscribed on
      //   (i.e. to get tokens, you need to know ID and channel for the pico making request)
      getTokensForVehicle = function(id, return_tokens) {<commit_msg>Add password protection to showTokens()<commit_after>ruleset fuse_fleet_oauth {
    meta {
        name "Fuse Fleet OAuth"
        description <<
Ruleset for fleet OAuth stuff
        >>
	
	errors to b16x13

	use module b16x10 alias fuse_keys

        use module a169x625 alias CloudOS
        use module a169x676 alias pds
	use module b16x19 alias common
	use module b16x26 alias reports

	
	sharing on



	provides clientAccessToken,  refreshTokenForAccessToken, showTokens, forgetTokens, forgetAllTokens, tellOwner, // don't provide after debug
             isAuthorized, redirectUri, carvoyantOauthUrl, codeForAccessToken, getTokens, fixToken 

    }

    global {
    
      // duplicated here and in fuse_carvoyant.krl
      api_hostname = "api.carvoyant.com";
      apiHostname = function() {api_hostname};
      oauth_url = "https://"+api_hostname+"/oauth/token";

      // appears in both this ruleset and fuse_carvoyant
      namespace = function() {
        "fuse:carvoyant";
      };

      // ---------- authorization ----------

   
      // used for getting token to create an account; not for general use
      clientAccessToken = function() {
        header = 
            {"credentials": {
               "username": keys:carvoyant_client("client_id"),
               "password": keys:carvoyant_client("client_secret"),
	       "realm": apiHostname(),
	       "netloc": apiHostname() + ":443"
               },
             "params" : {"grant_type": "client_credentials"}
            }; //.klog(">>>>>> client header <<<<<<<<");
        raw_result = http:post(oauth_url, header);
        (raw_result{"status_code"} eq "200") => raw_result{"content"}.decode()
                                              | raw_result.decode()
      };

      redirectUri = function() {
        "https://" + meta:host() + "/sky/event/" + keys:anonymous_pico("eci")  + "/" + math:random(9999) +  "/oauth/new_oauth_code";
      } 



      // this function creates a carvoyant OAuth URL that leads the user to a login screen. 
      // the redirect URL gets picked up by the anonymous pico's CloudOS handleOauthCode rule 
      carvoyantOauthUrl = function(hostsite) {
    
        redirect_uri = redirectUri();
        accessing_eci = meta:eci();

        params = {"client_id" : keys:carvoyant_client("client_id"),	
                  "redirect_uri" : redirect_uri,
  		  "response_type" : "code",
  		  "state": [meta:rid(), accessing_eci, hostsite ].join(",")
		 };

        query_string = params.map(function(k,v){k+"="+v}).values().join("&").klog(">>>>> query string >>>>>>");
        {"url": "https://auth.carvoyant.com/OAuth/authorize?" + query_string}
      }



      // CloudOS:handleOauthCode rule redirects to this function based on state param created in carvoyantOathUrl()
      codeForAccessToken = function(code, redirect_uri, hostsite) {
        header = 
            {"credentials": {
               "username": keys:carvoyant_client("client_id"),
               "password": <PASSWORD>:<PASSWORD>("<PASSWORD>_secret"),
	       "realm": apiHostname(),
	       "netloc": apiHostname() + ":443"
               },
             "params" : {"grant_type": "authorization_code",
	                 "code": code,
			 "redirect_uri": redirect_uri
	                }
            }; //.klog(">>>>>> client header <<<<<<<<");
        raw_result = http:post(oauth_url, header);
        results = (raw_result{"status_code"} eq "200") => normalizeAccountInfo(raw_result{"content"}.decode())
                                                        | raw_result.decode();

        // hardcoded URL!!
        url = hostsite + "?" +
                results.map(function(k,v){k + "=" + v}).values().join("&").klog(">>>>> url >>>>>");
        
        page = <<
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title></title>
  <META HTTP-EQUIV="Refresh" CONTENT="0;url=#{url}">
  <meta name="robots" content="noindex"/>
  <link rel="canonical" href="#{url}"/>
</head>
<body>
<p>
You are being redirected to <a href="#{url}">#{url}</a>
</p>

</body>
</html>
        >>;

        page
      };

    fixToken = function() {
      account_info = ent:account_info || {};
      try_refresh = not account_info{"refresh_token"}.isnull();
      new_tokens = try_refresh => refreshTokenForAccessToken().klog(">>>>> refreshing for carvoyant_get() >>> ")
                                | {};
      new_tokens
    };


      refreshTokenForAccessToken = function() {
        account_info = ent:account_info || {};
        header = 
            {"credentials": {
               "username": keys:carvoyant_client("client_id"),
               "password": keys:car<PASSWORD>ant_client("client_<PASSWORD>"),
	       "realm": apiHostname(),
	       "netloc": apiHostname() + ":443"
               },
             "params" : {"grant_type": "refresh_token",
	                 "refresh_token": account_info{"refresh_token"}
	                }
            }; //.klog(">>>>>> client header <<<<<<<<");
        raw_result = http:post(oauth_url, header).klog(">>> refresh request result >>> ");
        invalid_grant = raw_result{"status_code"} eq "400" && 
	                raw_result{"content"}.decode().pick("$.error") eq "invalid_grant";
        (raw_result{"status_code"} eq "200") => normalizeAccountInfo(raw_result{"content"}.decode()) |
        invalid_grant                        => tellOwner()
                                              | raw_result.decode()
      };

      normalizeAccountInfo = function(account_info) {

        // raise an event to broadcast new config
//        response = cloudos:sendEvent(meta:eci(), "fuse", "config_outdated", account_info);
	
        // add the timestamp and then store the info in an entity var (ugh; evil)
	account_info{"access_token"}.isnull() => ent:account_info |
                                                 account_info.put(["timestamp"], time:now()).pset(ent:account_info);
      }

      forgetTokens = function(){
        "".pset(ent:account_info{["access_token"]})
      };

      forgetAllTokens = function(){
        {}.pset(ent:account_info)
      };

      showTokens = function(password) {
	passwords_match = password eq keys:fuse_admin("password");
	passwords_match => getTokens()
                         | {"error": "access denied"}
      };

      // only give tokens to pico who identify themselves and we confirm they came in on the channel
      //   that they are subscribed on
      //   (i.e. to get tokens, you need to know ID and channel for the pico making request)
      getTokensForVehicle = function(id, return_tokens) {
{'fraction_non_alphanumeric': 0.1474006116207951, 'fraction_numerical': 0.008409785932721712, 'mean_word_length': 2.2531708530216363, 'pattern_counts': {'":': 53, '<': 98, '<?xml version=': 0, '>': 169, 'https://': 6, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 18, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '83908', 'n_tokens_mistral': 3958, 'n_tokens_neox': 3739, 'n_words': 828}