Skip to content

Commit

Permalink
Support basic auth for API gateways (#75)
Browse files Browse the repository at this point in the history
* Add CDK_EXTERNAL_USER/CDK_EXTERNAL_PASSWORD to support basic auth for API gateways

* EXTERNAL -> REMOTE_AUTH

* Introduce auth mode

* doc and validation of auth mode
  • Loading branch information
gbecan authored Dec 6, 2024
1 parent 1c3244b commit 9f232e2
Show file tree
Hide file tree
Showing 3 changed files with 179 additions and 37 deletions.
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,24 @@ CDK_GATEWAY_USER=admin
CDK_GATEWAY_PASSWORD=conduktor
````

### Configuring the CLI for authenticating through an API Gateway
Console has the ability to delegate the authentication to an API Gateway.
To provide the credentials to the API Gateway you can either use a bearer token:
````yaml
CDK_BASE_URL=http://localhost:8080
CDK_AUTH_MODE=external
CDK_API_KEY=<token>
````

or basic auth:
````yaml
CDK_BASE_URL=http://localhost:8080
CDK_AUTH_MODE=external
CDK_USER=<client_id>
CDK_PASSWORD=<client_secret>
````


### Commands Usage
````
You need to define the CDK_API_KEY and CDK_BASE_URL environment variables to use this tool.
Expand Down
51 changes: 50 additions & 1 deletion client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func TestApplyShouldWork(t *testing.T) {
if err != nil {
panic(err)
}
client.setApiKeyFromEnvIfNeeded()
client.setAuthMethodFromEnvIfNeeded()
httpmock.ActivateNonDefault(
client.client.GetClient(),
)
Expand Down Expand Up @@ -77,6 +77,55 @@ func TestApplyShouldWork(t *testing.T) {
}
}

func TestApplyShouldWorkWithExternalAuthMode(t *testing.T) {
defer httpmock.Reset()
baseUrl := "http://baseUrl"
user := "user"
password := "password"
client, err := Make(ApiParameter{
BaseUrl: baseUrl,
CdkUser: user,
CdkPassword: password,
AuthMode: "ExTerNaL",
})
if err != nil {
panic(err)
}
client.setAuthMethodFromEnvIfNeeded()
httpmock.ActivateNonDefault(
client.client.GetClient(),
)
responder := httpmock.NewStringResponder(200, `{"upsertResult": "NotChanged"}`)

topic := resource.Resource{
Json: []byte(`{"yolo": "data"}`),
Kind: "Topic",
Name: "toto",
Version: "v2",
Metadata: map[string]interface{}{
"cluster": "local",
},
}

httpmock.RegisterMatcherResponderWithQuery(
"PUT",
"http://baseUrl/api/public/kafka/v2/cluster/local/topic",
nil,
httpmock.HeaderIs("Authorization", "Basic dXNlcjpwYXNzd29yZA==").
And(httpmock.HeaderIs("X-CDK-CLIENT", "CLI/unknown")).
And(httpmock.BodyContainsBytes(topic.Json)),
responder,
)

body, err := client.Apply(&topic, false)
if err != nil {
t.Error(err)
}
if body != "NotChanged" {
t.Errorf("Bad result expected NotChanged got: %s", body)
}
}

func TestApplyWithDryModeShouldWork(t *testing.T) {
defer httpmock.Reset()
baseUrl := "http://baseUrl"
Expand Down
147 changes: 111 additions & 36 deletions client/console_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package client

import (
"crypto/tls"
"encoding/base64"
"encoding/json"
"fmt"
"os"
Expand All @@ -14,11 +15,34 @@ import (
"github.com/go-resty/resty/v2"
)

type AuthMethod interface {
AuthorizationHeader() string
}

type BearerToken struct {
Token string
}

func (t BearerToken) AuthorizationHeader() string {
return fmt.Sprintf("Bearer %s", t.Token)
}

type BasicAuth struct {
Username string
Password string
}

func (t BasicAuth) AuthorizationHeader() string {
credentials := fmt.Sprintf("%s:%s", t.Username, t.Password)
encodedCredentials := base64.StdEncoding.EncodeToString([]byte(credentials))
return fmt.Sprintf("Basic %s", encodedCredentials)
}

type Client struct {
apiKey string
baseUrl string
client *resty.Client
kinds schema.KindCatalog
authMethod AuthMethod
baseUrl string
client *resty.Client
kinds schema.KindCatalog
}

type ApiParameter struct {
Expand All @@ -30,6 +54,7 @@ type ApiParameter struct {
Cacert string
CdkUser string
CdkPassword string
AuthMode string
Insecure bool
}

Expand Down Expand Up @@ -60,6 +85,7 @@ func Make(apiParameter ApiParameter) (*Client, error) {
if (apiParameter.CdkUser != "" && apiParameter.CdkPassword == "") || (apiParameter.CdkUser == "" && apiParameter.CdkPassword != "") {
return nil, fmt.Errorf("CDK_USER and CDK_PASSWORD must be provided together")
}

if apiParameter.CdkUser != "" && apiParameter.ApiKey != "" {
return nil, fmt.Errorf("Can't set both CDK_USER and CDK_API_KEY")
}
Expand All @@ -69,29 +95,40 @@ func Make(apiParameter ApiParameter) (*Client, error) {
}

result := &Client{
apiKey: apiParameter.ApiKey,
baseUrl: uniformizeBaseUrl(apiParameter.BaseUrl),
client: restyClient,
kinds: nil,
authMethod: nil,
baseUrl: uniformizeBaseUrl(apiParameter.BaseUrl),
client: restyClient,
kinds: nil,
}

if apiParameter.Insecure {
result.IgnoreUntrustedCertificate()
}

if apiParameter.ApiKey != "" {
result.authMethod = BearerToken{apiParameter.ApiKey}
}

if apiParameter.CdkUser != "" {
jwtToken, err := result.Login(apiParameter.CdkUser, apiParameter.CdkPassword)
if err != nil {
return nil, fmt.Errorf("Could not login: %s", err)
if strings.ToLower(apiParameter.AuthMode) == "external" {
result.authMethod = BasicAuth{apiParameter.CdkUser, apiParameter.CdkPassword}
} else if apiParameter.AuthMode == "" || strings.ToLower(apiParameter.AuthMode) == "conduktor" {
jwtToken, err := result.Login(apiParameter.CdkUser, apiParameter.CdkPassword)
if err != nil {
return nil, fmt.Errorf("Could not login: %s", err)
}
bearer := BearerToken{jwtToken.AccessToken}
result.authMethod = &bearer
} else {
return nil, fmt.Errorf("CDK_AUTH_MODE was: \"%s\". Accepted values are \"conduktor\" or \"external\".", apiParameter.AuthMode)
}
result.apiKey = jwtToken.AccessToken
}

if result.apiKey != "" {
result.setApiKeyInRestClient()
if result.authMethod != nil {
result.setAuthMethodInRestClient()
} else {
//it will be set later only when really needed
//so aim is not fail when CDK_API_KEY is not set before printing the cmd help
//so aim is not fail when auth method is not set before printing the cmd help
}

err := result.initKindFromApi()
Expand All @@ -113,6 +150,7 @@ func MakeFromEnv() (*Client, error) {
ApiKey: os.Getenv("CDK_API_KEY"),
CdkUser: os.Getenv("CDK_USER"),
CdkPassword: os.Getenv("CDK_PASSWORD"),
AuthMode: os.Getenv("CDK_AUTH_MODE"),
Insecure: strings.ToLower(os.Getenv("CDK_INSECURE")) == "true",
}

Expand All @@ -131,25 +169,62 @@ func (c *Client) IgnoreUntrustedCertificate() {
c.client.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true})
}

func (c *Client) setApiKeyFromEnvIfNeeded() {
if c.apiKey == "" {
func (c *Client) setAuthMethodFromEnvIfNeeded() {
if c.authMethod == nil {
authMode := strings.ToLower(os.Getenv("CDK_AUTH_MODE"))
apiKey := os.Getenv("CDK_API_KEY")
if apiKey == "" {
fmt.Fprintln(os.Stderr, "Please set CDK_API_KEY")

if authMode == "external" {
user := os.Getenv("CDK_USER")
password := os.Getenv("CDK_PASSWORD")

if apiKey == "" && user == "" {
fmt.Fprintln(os.Stderr, "Please set CDK_API_KEY or CDK_USER/CDK_PASSWORD")
os.Exit(1)
}

if apiKey != "" && user != "" {
fmt.Fprintln(os.Stderr, "Can't set both CDK_API_KEY and CDK_USER")
os.Exit(1)
}

if user != "" && password == "" {
fmt.Fprintln(os.Stderr, "Please set CDK_PASSWORD when using CDK_USER")
os.Exit(1)
}

if apiKey != "" {
c.authMethod = BearerToken{apiKey}
} else {
c.authMethod = BasicAuth{user, password}
}
} else if authMode == "" || authMode == "conduktor" {
if apiKey == "" {
fmt.Fprintln(os.Stderr, "Please set CDK_API_KEY")
os.Exit(1)
}

c.authMethod = BearerToken{apiKey}
} else {
fmt.Fprintf(os.Stderr, "CDK_AUTH_MODE was: \"%s\". Accepted values are \"conduktor\" or \"external\"\n.", authMode)
os.Exit(1)
}
c.apiKey = apiKey
c.setApiKeyInRestClient()

c.setAuthMethodInRestClient()
}
}

func (c *Client) setApiKeyInRestClient() {
c.client = c.client.SetHeader("Authorization", "Bearer "+c.apiKey)
func (c *Client) setAuthMethodInRestClient() {
if c.authMethod == nil {
fmt.Fprintln(os.Stderr, "No authentication method defined. Please set CDK_API_KEY or CDK_USER/CDK_PASSWORD")
os.Exit(1)
}
c.client = c.client.SetHeader("Authorization", c.authMethod.AuthorizationHeader())
}

func (c *Client) SetApiKey(apiKey string) {
c.apiKey = apiKey
c.setApiKeyInRestClient()
c.authMethod = BearerToken{apiKey}
c.setAuthMethodInRestClient()
}

func extractApiError(resp *resty.Response) string {
Expand All @@ -171,7 +246,7 @@ func (client *Client) ActivateDebug() {
}

func (client *Client) Apply(resource *resource.Resource, dryMode bool) (string, error) {
client.setApiKeyFromEnvIfNeeded()
client.setAuthMethodFromEnvIfNeeded()
kinds := client.GetKinds()
kind, ok := kinds[resource.Kind]
if !ok {
Expand Down Expand Up @@ -204,7 +279,7 @@ func (client *Client) Apply(resource *resource.Resource, dryMode bool) (string,

func (client *Client) Get(kind *schema.Kind, parentPathValue []string, queryParams map[string]string) ([]resource.Resource, error) {
var result []resource.Resource
client.setApiKeyFromEnvIfNeeded()
client.setAuthMethodFromEnvIfNeeded()
url := client.baseUrl + kind.ListPath(parentPathValue)
requestBuilder := client.client.R()
if queryParams != nil {
Expand Down Expand Up @@ -243,7 +318,7 @@ func (client *Client) Login(username, password string) (LoginResult, error) {

func (client *Client) Describe(kind *schema.Kind, parentPathValue []string, name string) (resource.Resource, error) {
var result resource.Resource
client.setApiKeyFromEnvIfNeeded()
client.setAuthMethodFromEnvIfNeeded()
url := client.baseUrl + kind.DescribePath(parentPathValue, name)
resp, err := client.client.R().Get(url)
if err != nil {
Expand All @@ -256,7 +331,7 @@ func (client *Client) Describe(kind *schema.Kind, parentPathValue []string, name
}

func (client *Client) Delete(kind *schema.Kind, parentPathValue []string, name string) error {
client.setApiKeyFromEnvIfNeeded()
client.setAuthMethodFromEnvIfNeeded()
url := client.baseUrl + kind.DescribePath(parentPathValue, name)
resp, err := client.client.R().Delete(url)
if err != nil {
Expand All @@ -271,7 +346,7 @@ func (client *Client) Delete(kind *schema.Kind, parentPathValue []string, name s
}

func (client *Client) DeleteResource(resource *resource.Resource) error {
client.setApiKeyFromEnvIfNeeded()
client.setAuthMethodFromEnvIfNeeded()
kinds := client.GetKinds()
kind, ok := kinds[resource.Kind]
if !ok {
Expand Down Expand Up @@ -324,7 +399,7 @@ func (client *Client) initKindFromApi() error {

func (client *Client) ListAdminToken() ([]Token, error) {
result := make([]Token, 0)
client.setApiKeyFromEnvIfNeeded()
client.setAuthMethodFromEnvIfNeeded()
url := client.baseUrl + "/token/v1/admin_tokens"
resp, err := client.client.R().Get(url)
if err != nil {
Expand All @@ -339,7 +414,7 @@ func (client *Client) ListAdminToken() ([]Token, error) {

func (client *Client) ListApplicationInstanceToken(applicationInstanceName string) ([]Token, error) {
result := make([]Token, 0)
client.setApiKeyFromEnvIfNeeded()
client.setAuthMethodFromEnvIfNeeded()
url := client.baseUrl + "/token/v1/application_instance_tokens/" + applicationInstanceName
resp, err := client.client.R().Get(url)
if err != nil {
Expand All @@ -354,7 +429,7 @@ func (client *Client) ListApplicationInstanceToken(applicationInstanceName strin

func (client *Client) CreateAdminToken(name string) (CreatedToken, error) {
var result CreatedToken
client.setApiKeyFromEnvIfNeeded()
client.setAuthMethodFromEnvIfNeeded()
url := client.baseUrl + "/token/v1/admin_tokens"
resp, err := client.client.R().SetBody(map[string]string{"name": name}).Post(url)
if err != nil {
Expand All @@ -369,7 +444,7 @@ func (client *Client) CreateAdminToken(name string) (CreatedToken, error) {

func (client *Client) CreateApplicationInstanceToken(applicationInstanceName, name string) (CreatedToken, error) {
var result CreatedToken
client.setApiKeyFromEnvIfNeeded()
client.setAuthMethodFromEnvIfNeeded()
url := client.baseUrl + "/token/v1/application_instance_tokens/" + applicationInstanceName
resp, err := client.client.R().SetBody(map[string]string{"name": name}).Post(url)
if err != nil {
Expand All @@ -389,7 +464,7 @@ type SqlResult struct {

func (client *Client) ExecuteSql(maxLine int, sql string) (SqlResult, error) {
var result SqlResult
client.setApiKeyFromEnvIfNeeded()
client.setAuthMethodFromEnvIfNeeded()
url := client.baseUrl + "/public/sql/v1/execute"
resp, err := client.client.R().SetBody(sql).SetQueryParam("maxLine", fmt.Sprintf("%d", maxLine)).Post(url)
if err != nil {
Expand All @@ -403,7 +478,7 @@ func (client *Client) ExecuteSql(maxLine int, sql string) (SqlResult, error) {
}

func (client *Client) DeleteToken(uuid string) error {
client.setApiKeyFromEnvIfNeeded()
client.setAuthMethodFromEnvIfNeeded()
url := client.baseUrl + "/token/v1/" + uuid
resp, err := client.client.R().Delete(url)
if err != nil {
Expand Down

0 comments on commit 9f232e2

Please sign in to comment.