diff --git a/build/main.go b/build/main.go index 4c047e97..799505ad 100644 --- a/build/main.go +++ b/build/main.go @@ -119,6 +119,9 @@ func getTestDependencies(ctx context.Context, client *dagger.Client, hostDirecto WithUser("flipt"). WithEnvVariable("FLIPT_STORAGE_TYPE", "local"). WithEnvVariable("FLIPT_STORAGE_LOCAL_PATH", "/var/data/flipt"). + WithEnvVariable("FLIPT_AUTHENTICATION_METHODS_TOKEN_ENABLED", "1"). + WithEnvVariable("FLIPT_AUTHENTICATION_METHODS_TOKEN_BOOTSTRAP_TOKEN", "secret"). + WithEnvVariable("FLIPT_AUTHENTICATION_REQUIRED", "1"). WithExposedPort(8080) return flipt, rust.File("/app/target/release/libfliptengine.so"), rust.File("/app/target/release/flipt_engine.h") @@ -133,6 +136,7 @@ func pythonTests(ctx context.Context, client *dagger.Client, flipt *dagger.Conta WithFile("/app/libfliptengine.so", dynamicLibrary). WithServiceBinding("flipt", flipt.WithExec(nil).AsService()). WithEnvVariable("FLIPT_URL", "http://flipt:8080"). + WithEnvVariable("FLIPT_AUTH_TOKEN", "secret"). WithEnvVariable("FLIPT_ENGINE_LIB_PATH", "/app/libfliptengine.so"). WithExec([]string{"poetry", "install", "--without=dev"}). WithExec([]string{"poetry", "run", "test"}). @@ -152,6 +156,7 @@ func goTests(ctx context.Context, client *dagger.Client, flipt *dagger.Container WithFile("/app/flipt_engine.h", headerFile). WithServiceBinding("flipt", flipt.WithExec(nil).AsService()). WithEnvVariable("FLIPT_URL", "http://flipt:8080"). + WithEnvVariable("FLIPT_AUTH_TOKEN", "secret"). // Since the dynamic library is being sourced from a "non-standard location" we can // modify the LD_LIBRARY_PATH variable to inform the linker different locations for // dynamic libraries. @@ -175,6 +180,7 @@ func nodeTests(ctx context.Context, client *dagger.Client, flipt *dagger.Contain WithFile("/app/libfliptengine.so", dynamicLibrary). WithServiceBinding("flipt", flipt.WithExec(nil).AsService()). WithEnvVariable("FLIPT_URL", "http://flipt:8080"). + WithEnvVariable("FLIPT_AUTH_TOKEN", "secret"). WithEnvVariable("FLIPT_ENGINE_LIB_PATH", "/app/libfliptengine.so"). WithExec([]string{"npm", "install"}). WithExec([]string{"npm", "test"}). @@ -191,6 +197,7 @@ func rubyTests(ctx context.Context, client *dagger.Client, flipt *dagger.Contain WithFile("/app/lib/ext/libfliptengine.so", dynamicLibrary). WithServiceBinding("flipt", flipt.WithExec(nil).AsService()). WithEnvVariable("FLIPT_URL", "http://flipt:8080"). + WithEnvVariable("FLIPT_AUTH_TOKEN", "secret"). WithExec([]string{"bundle", "install"}). WithExec([]string{"bundle", "exec", "rspec"}). Sync(ctx) diff --git a/flipt-client-go/README.md b/flipt-client-go/README.md index fc913528..66e09923 100644 --- a/flipt-client-go/README.md +++ b/flipt-client-go/README.md @@ -42,6 +42,7 @@ func main() { // evaluation.WithNamespace(string): configures which namespace you will be making evaluations on // evaluation.WithURL(string): configures which upstream Flipt data should be fetched from // evaluation.WithUpdateInterval(int): configures how often data should be fetched from the upstream + // evaluation.WithAuthToken(string): configures an auth token if your upstream Flipt instance requires it evaluationClient, err := evaluation.NewClient() if err != nil { log.Fatal(err) diff --git a/flipt-client-go/evaluation.go b/flipt-client-go/evaluation.go index f2c75f7a..c059857a 100644 --- a/flipt-client-go/evaluation.go +++ b/flipt-client-go/evaluation.go @@ -18,6 +18,7 @@ type Client struct { engine unsafe.Pointer namespace string url string + authToken string updateInterval int } @@ -34,6 +35,7 @@ func NewClient(opts ...clientOption) (*Client, error) { engOpts := &EngineOpts{ URL: client.url, UpdateInterval: client.updateInterval, + AuthToken: client.authToken, } b, err := json.Marshal(engOpts) @@ -83,6 +85,13 @@ func WithUpdateInterval(updateInterval int) clientOption { } } +// WithAuthToken allows for configuring an auth token to communicate with a protected Flipt server. +func WithAuthToken(authToken string) clientOption { + return func(c *Client) { + c.authToken = authToken + } +} + // EvaluateVariant makes an evaluation on a variant flag. func (e *Client) EvaluateVariant(_ context.Context, flagKey, entityID string, evalContext map[string]string) (*VariantResult, error) { ereq, err := json.Marshal(evaluationRequest{ diff --git a/flipt-client-go/evaluation_test.go b/flipt-client-go/evaluation_test.go index 54f838fe..25e28bbe 100644 --- a/flipt-client-go/evaluation_test.go +++ b/flipt-client-go/evaluation_test.go @@ -11,16 +11,22 @@ import ( ) var fliptUrl string +var authToken string func init() { fliptUrl = os.Getenv("FLIPT_URL") if fliptUrl == "" { panic("set FLIPT_URL") } + + authToken = os.Getenv("FLIPT_AUTH_TOKEN") + if authToken == "" { + panic("set FLIPT_AUTH_TOKEN") + } } func TestVariant(t *testing.T) { - evaluationClient, err := evaluation.NewClient(evaluation.WithURL(fliptUrl)) + evaluationClient, err := evaluation.NewClient(evaluation.WithURL(fliptUrl), evaluation.WithAuthToken(authToken)) require.NoError(t, err) variant, err := evaluationClient.EvaluateVariant(context.TODO(), "flag1", "someentity", map[string]string{ @@ -37,7 +43,7 @@ func TestVariant(t *testing.T) { } func TestBoolean(t *testing.T) { - evaluationClient, err := evaluation.NewClient(evaluation.WithURL(fliptUrl)) + evaluationClient, err := evaluation.NewClient(evaluation.WithURL(fliptUrl), evaluation.WithAuthToken(authToken)) require.NoError(t, err) boolean, err := evaluationClient.EvaluateBoolean(context.TODO(), "flag_boolean", "someentity", map[string]string{ diff --git a/flipt-client-go/models.go b/flipt-client-go/models.go index 59bd468b..22548225 100644 --- a/flipt-client-go/models.go +++ b/flipt-client-go/models.go @@ -9,6 +9,7 @@ type evaluationRequest struct { type EngineOpts struct { URL string `json:"url,omitempty"` + AuthToken string `json:"auth_token,omitempty"` UpdateInterval int `json:"update_interval,omitempty"` } diff --git a/flipt-client-node/README.md b/flipt-client-node/README.md index 416c66ba..90571dbf 100644 --- a/flipt-client-node/README.md +++ b/flipt-client-node/README.md @@ -27,11 +27,12 @@ import { FliptEvaluationClient } from 'flipt-client-node'; // engine_opts is the second positional argument and is also optional, the structure is: // { // "url": "http://localhost:8080", -// "update_interval": 120 +// "update_interval": 120, +// "auth_token": "secret" // } // -// You can replace the url with where your upstream Flipt instance points to, and the update interval for how long you are willing -// to wait for updated flag state. +// You can replace the url with where your upstream Flipt instance points to, the update interval for how long you are willing +// to wait for updated flag state, and the auth token if your Flipt instance requires it. const fliptEvaluationClient = new FliptEvaluationClient(); const variant = fliptEvaluationClient.evaluateVariant("flag1", "someentity", {"fizz": "buzz"}); diff --git a/flipt-client-node/package-lock.json b/flipt-client-node/package-lock.json index 17ec2542..d952630c 100644 --- a/flipt-client-node/package-lock.json +++ b/flipt-client-node/package-lock.json @@ -7,7 +7,7 @@ "": { "name": "flipt-client-node", "version": "0.0.1", - "license": "ISC", + "license": "MIT", "dependencies": { "ffi-napi": "^4.0.3", "ref-napi": "^3.0.3" diff --git a/flipt-client-node/src/evaluation.test.ts b/flipt-client-node/src/evaluation.test.ts index 500df633..93ea6ae2 100644 --- a/flipt-client-node/src/evaluation.test.ts +++ b/flipt-client-node/src/evaluation.test.ts @@ -6,8 +6,17 @@ if (!fliptUrl) { process.exit(1); } +const authToken = process.env['FLIPT_AUTH_TOKEN']; +if (!authToken) { + console.error('please set the FLIPT_AUTH_TOKEN environment variable'); + process.exit(1); +} + test('variant', () => { - const fec = new FliptEvaluationClient('default', { url: fliptUrl }); + const fec = new FliptEvaluationClient('default', { + url: fliptUrl, + auth_token: authToken + }); const variant = fec.evaluateVariant('flag1', 'someentity', { fizz: 'buzz' }); @@ -22,7 +31,10 @@ test('variant', () => { }); test('boolean', () => { - const fec = new FliptEvaluationClient('default', { url: fliptUrl }); + const fec = new FliptEvaluationClient('default', { + url: fliptUrl, + auth_token: authToken + }); const boolean = fec.evaluateBoolean('flag_boolean', 'someentity', { fizz: 'buzz' diff --git a/flipt-client-node/src/index.ts b/flipt-client-node/src/index.ts index 49074058..c622b716 100644 --- a/flipt-client-node/src/index.ts +++ b/flipt-client-node/src/index.ts @@ -25,7 +25,8 @@ export class FliptEvaluationClient { namespace?: string, engine_opts: EngineOpts = { url: 'http://localhost:8080', - update_interval: 120 + update_interval: 120, + auth_token: '' } ) { const engine = engineLib.initialize_engine( diff --git a/flipt-client-node/src/models.ts b/flipt-client-node/src/models.ts index 8058fee9..09ecbbbc 100644 --- a/flipt-client-node/src/models.ts +++ b/flipt-client-node/src/models.ts @@ -8,6 +8,7 @@ interface EvaluationRequest { interface EngineOpts { url?: string; update_interval?: number; + auth_token?: string; } interface VariantEvaluationResponse { diff --git a/flipt-client-python/README.md b/flipt-client-python/README.md index 1abbd9c2..332e96ce 100644 --- a/flipt-client-python/README.md +++ b/flipt-client-python/README.md @@ -21,9 +21,9 @@ In your Python code you can import this client and use it as so: ```python from flipt_client_python import FliptEvaluationClient -# "namespace" and "engine_opts" are two keyword arguments that this constructor accepts. +# "namespace" and "engine_opts" are two keyword arguments that this constructor accepts # namespace: which namespace to fetch flag state from -# engine_opts: follows the model EngineOpts in the models.py file. Configures the url of the upstream Flipt instance, and the interval in which to fetch new flag state +# engine_opts: follows the model EngineOpts in the models.py file. Configures the url of the upstream Flipt instance, the interval in which to fetch new flag state, and the auth token if your upstream Flipt instance requires it flipt_evaluation_client = FliptEvaluationClient() variant_result = flipt_evaluation_client.evaluate_variant(flag_key="flag1", entity_id="entity", context={"fizz": "buzz"}) diff --git a/flipt-client-python/flipt_client_python/models.py b/flipt-client-python/flipt_client_python/models.py index cfce61c8..cf6f651c 100644 --- a/flipt-client-python/flipt_client_python/models.py +++ b/flipt-client-python/flipt_client_python/models.py @@ -12,6 +12,7 @@ class EvaluationRequest(BaseModel): class EngineOpts(BaseModel): url: Optional[str] = None update_interval: Optional[int] = None + auth_token: Optional[str] = None class VariantEvaluationResponse(BaseModel): diff --git a/flipt-client-python/tests/__init__.py b/flipt-client-python/tests/__init__.py index 996c3218..72b1e01a 100644 --- a/flipt-client-python/tests/__init__.py +++ b/flipt-client-python/tests/__init__.py @@ -10,8 +10,12 @@ def setUp(self) -> None: if engine_url is None: raise Exception("FLIPT_URL not set") + auth_token = os.environ.get("FLIPT_AUTH_TOKEN") + if auth_token is None: + raise Exception("FLIPT_AUTH_TOKEN not set") + self.flipt_client = FliptEvaluationClient( - engine_opts=EngineOpts(url=engine_url) + engine_opts=EngineOpts(url=engine_url, auth_token=auth_token) ) def test_variant(self): diff --git a/flipt-client-ruby/spec/evaluation_spec.rb b/flipt-client-ruby/spec/evaluation_spec.rb index a656f192..69e7f1ce 100644 --- a/flipt-client-ruby/spec/evaluation_spec.rb +++ b/flipt-client-ruby/spec/evaluation_spec.rb @@ -6,7 +6,8 @@ describe '#initialize' do it 'initializes the engine' do url = ENV.fetch('FLIPT_URL', 'http://localhost:8080') - client = Flipt::EvaluationClient.new('default', { url: url }) + auth_token = ENV.fetch('FLIPT_AUTH_TOKEN', 'secret') + client = Flipt::EvaluationClient.new('default', { url: url, auth_token: auth_token }) expect(client).to be_a(Flipt::EvaluationClient) end end @@ -14,7 +15,8 @@ describe '#evaluate_variant' do it 'returns a variant result' do url = ENV.fetch('FLIPT_URL', 'http://localhost:8080') - client = Flipt::EvaluationClient.new('default', { url: url }) + auth_token = ENV.fetch('FLIPT_AUTH_TOKEN', 'secret') + client = Flipt::EvaluationClient.new('default', { url: url, auth_token: auth_token }) resp = client.evaluate_variant({ flag_key: 'flag1', entity_id: 'someentity', context: { "fizz": 'buzz' } }) @@ -32,7 +34,8 @@ describe '#evaluate_boolean' do it 'returns a boolean result' do url = ENV.fetch('FLIPT_URL', 'http://localhost:8080') - client = Flipt::EvaluationClient.new('default', { url: url }) + auth_token = ENV.fetch('FLIPT_AUTH_TOKEN', 'secret') + client = Flipt::EvaluationClient.new('default', { url: url, auth_token: auth_token }) resp = client.evaluate_boolean({ flag_key: 'flag_boolean', entity_id: 'someentity', context: { "fizz": 'buzz' } }) diff --git a/flipt-engine/examples/evaluation.rs b/flipt-engine/examples/evaluation.rs index e37c22f9..c594c0a4 100644 --- a/flipt-engine/examples/evaluation.rs +++ b/flipt-engine/examples/evaluation.rs @@ -6,7 +6,7 @@ use std::collections::HashMap; fn main() { let evaluator = evaluator::Evaluator::new_snapshot_evaluator( vec!["default".into()], - parser::HTTPParser::new("http://localhost:8080"), + parser::HTTPParser::new("http://localhost:8080", Some("secret")), ); let eng = fliptengine::Engine::new(evaluator.unwrap(), Default::default()); diff --git a/flipt-engine/src/evaluator/mod.rs b/flipt-engine/src/evaluator/mod.rs index 0bf16e88..3d576c9a 100644 --- a/flipt-engine/src/evaluator/mod.rs +++ b/flipt-engine/src/evaluator/mod.rs @@ -876,7 +876,7 @@ mod tests { "blah", ); - assert!(!result.is_ok()); + assert!(result.is_err()); assert_eq!( result.err().unwrap().to_string(), "error parsing boolean blah: err provided string was not `true` or `false`" @@ -895,7 +895,7 @@ mod tests { "notanumber", ); - assert!(!result_one.is_ok()); + assert!(result_one.is_err()); assert_eq!( result_one.err().unwrap().to_string(), "error parsing number notanumber, err: invalid digit found in string" @@ -911,7 +911,7 @@ mod tests { "9", ); - assert!(!result_two.is_ok()); + assert!(result_two.is_err()); assert_eq!( result_two.err().unwrap().to_string(), "error parsing number notanumber, err: invalid digit found in string" @@ -930,7 +930,7 @@ mod tests { "2006-01-02T15:04:05Z", ); - assert!(!result_one.is_ok()); + assert!(result_one.is_err()); assert_eq!( result_one.err().unwrap().to_string(), "error parsing time blah, err: input contains invalid characters" @@ -946,7 +946,7 @@ mod tests { "blah", ); - assert!(!result_two.is_ok()); + assert!(result_two.is_err()); assert_eq!( result_two.err().unwrap().to_string(), "error parsing time blah, err: input contains invalid characters" @@ -1028,7 +1028,7 @@ mod tests { let v = variant.unwrap(); assert_eq!(v.flag_key, String::from("foo")); - assert_eq!(v.r#match, true); + assert!(v.r#match); assert_eq!(v.reason, common::EvaluationReason::Match); assert_eq!(v.segment_keys, vec![String::from("segment1")]); } @@ -1671,7 +1671,7 @@ mod tests { namespace_key: String::from("default"), flag_key: String::from("foo"), entity_id: String::from("01"), - context: context, + context, }); assert!(variant.is_ok()); @@ -1759,7 +1759,7 @@ mod tests { let v = variant.unwrap(); assert_eq!(v.flag_key, String::from("foo")); - assert_eq!(v.r#match, true); + assert!(v.r#match); assert_eq!(v.reason, common::EvaluationReason::Match); assert_eq!(v.segment_keys, vec![String::from("segment1")]); } @@ -2407,7 +2407,7 @@ mod tests { namespace_key: String::from("default"), flag_key: String::from("foo"), entity_id: String::from("01"), - context: context, + context, }); assert!(variant.is_ok()); diff --git a/flipt-engine/src/lib.rs b/flipt-engine/src/lib.rs index 0600ff06..d3a00576 100644 --- a/flipt-engine/src/lib.rs +++ b/flipt-engine/src/lib.rs @@ -69,6 +69,7 @@ fn result_to_json_ptr(result: Result) -> *mut c_char #[derive(Deserialize)] pub struct EngineOpts { url: Option, + auth_token: Option, update_interval: Option, } @@ -76,6 +77,7 @@ impl Default for EngineOpts { fn default() -> Self { Self { url: Some("http://localhost:8080".into()), + auth_token: None, update_interval: Some(120), } } @@ -179,7 +181,9 @@ pub unsafe extern "C" fn initialize_engine( .to_owned() .unwrap_or("http://localhost:8080".into()); - let parser = parser::HTTPParser::new(&http_url); + let auth_token = engine_opts.auth_token.to_owned(); + + let parser = parser::HTTPParser::new(&http_url, auth_token.clone().as_deref()); let evaluator = evaluator::Evaluator::new_snapshot_evaluator(namespaces_vec, parser); Box::into_raw(Box::new(Engine::new(evaluator.unwrap(), engine_opts))) as *mut c_void diff --git a/flipt-engine/src/parser/mod.rs b/flipt-engine/src/parser/mod.rs index 8854ac22..1765e8a8 100644 --- a/flipt-engine/src/parser/mod.rs +++ b/flipt-engine/src/parser/mod.rs @@ -9,10 +9,12 @@ pub trait Parser { pub struct HTTPParser { http_client: reqwest::blocking::Client, http_url: String, + auth_token: Option, } impl HTTPParser { - pub fn new(url: &str) -> Self { + // TODO: potentially use builder pattern if we need to add more options + pub fn new(url: &str, auth_token: Option<&str>) -> Self { // We will allow the following line to panic when an error is encountered. let http_client = reqwest::blocking::Client::builder() .timeout(std::time::Duration::from_secs(10)) @@ -22,18 +24,37 @@ impl HTTPParser { Self { http_client, http_url: url.to_string(), + auth_token: auth_token.unwrap_or_default().to_string().into(), } } } impl Parser for HTTPParser { fn parse(&self, namespace: &str) -> Result { + let mut headers = reqwest::header::HeaderMap::new(); + headers.insert( + reqwest::header::CONTENT_TYPE, + reqwest::header::HeaderValue::from_static("application/json"), + ); + headers.insert( + reqwest::header::ACCEPT, + reqwest::header::HeaderValue::from_static("application/json"), + ); + + if let Some(ref token) = self.auth_token { + headers.insert( + reqwest::header::AUTHORIZATION, + reqwest::header::HeaderValue::from_str(&format!("Bearer {}", token)).unwrap(), + ); + } + let response = match self .http_client .get(format!( "{}/internal/v1/evaluation/snapshot/namespace/{}", self.http_url, namespace )) + .headers(headers) .send() { Ok(resp) => resp, diff --git a/flipt-engine/src/store/mod.rs b/flipt-engine/src/store/mod.rs index 933fa8b0..5ba20605 100644 --- a/flipt-engine/src/store/mod.rs +++ b/flipt-engine/src/store/mod.rs @@ -278,7 +278,7 @@ mod tests { .expect("flag1 should exist"); assert_eq!(flag_variant.key, "flag1"); - assert_eq!(flag_variant.enabled, true); + assert!(flag_variant.enabled); assert_eq!(flag_variant.r#type, common::FlagType::Variant); let flag_boolean = snapshot @@ -286,7 +286,7 @@ mod tests { .expect("flag_boolean should exist"); assert_eq!(flag_boolean.key, "flag_boolean"); - assert_eq!(flag_boolean.enabled, true); + assert!(flag_boolean.enabled); assert_eq!(flag_boolean.r#type, common::FlagType::Boolean); let evaluation_rules = snapshot @@ -342,7 +342,7 @@ mod tests { .as_ref() .expect("first rollout should be segment"); - assert_eq!(segment_rollout.value, true); + assert!(segment_rollout.value); assert_eq!( segment_rollout.segment_operator, common::SegmentOperator::Or