Initial cloud-services repo - gateway service + pkg modules

This commit is contained in:
Chris Rai
2026-01-30 23:14:52 -05:00
commit fbb820d7b3
1037 changed files with 171318 additions and 0 deletions

View File

@@ -0,0 +1,28 @@
package superset
import (
"fmt"
"fiskerinc.com/modules/redis"
"github.com/pkg/errors"
)
var errTokenNotFound = errors.New("token isn't found")
func getCachedAccToken(c redis.Client) (string, error) {
res, err := c.Get(redis.SupersetAccTokenKey)
if err != nil {
return "", err
}
if res == nil {
return "", errors.WithStack(errTokenNotFound)
}
tokenBytes, ok := res.([]byte)
if !ok {
return "", errors.WithStack(fmt.Errorf("invalid superset access token type (expected []byte]); access token: %v", res))
}
return string(tokenBytes), nil
}

130
pkg/superset/dashboards.go Normal file
View File

@@ -0,0 +1,130 @@
package superset
import (
"encoding/json"
"net/http"
"net/url"
"strconv"
"fiskerinc.com/modules/logger"
"github.com/pkg/errors"
)
func GetEmbeddableDashboards(accToken string) (embeddableDashboards []EmbeddableDashboard, err error) {
publishedDashboards, err := getPublishedDashboards(accToken)
if err != nil {
return nil, err
}
embeddableDashboards = make([]EmbeddableDashboard, 0, len(publishedDashboards))
for _, dashboard := range publishedDashboards {
tempId, err := getDashboardsEmbeddedID(dashboard.ID, accToken)
// If one of the dashboards gets an error, that could be fine, as long as not all of them do
// The dashboard failed to have embedding data on it
if err != nil || tempId == "" {
continue
}
embeddableDashboards = append(embeddableDashboards, EmbeddableDashboard{Title: dashboard.DashboardTitle, EmbeddingId: tempId})
}
return embeddableDashboards, nil
}
func getPublishedDashboards(accToken string) (publishedDashboard []publishedDashboard, err error) {
// Host already has the /api/v1
targetURL, err := url.JoinPath(host, "dashboard")
if err != nil {
return nil, errors.WithStack(err)
}
// ulr.JoinPath with url escape the ?
// Filter that published == true
targetURL += "/?q=%7B%0A%20%20%22filters%22%3A%20%5B%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%22col%22%3A%20%22published%22%2C%0A%20%20%20%20%20%20%22opr%22%3A%20%22eq%22%2C%0A%20%20%20%20%20%20%22value%22%3A%20true%0A%20%20%20%20%7D%0A%20%20%5D%0A%7D"
req, err := http.NewRequest("GET", targetURL, nil)
if err != nil {
return nil, errors.WithStack(err)
}
req.Header.Set("Accept", "application/json")
req.Header.Add("Authorization", "Bearer "+accToken)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, errors.WithStack(err)
}
defer resp.Body.Close()
if resp.StatusCode == 404 {
logger.Warn().Msgf("404 while trying to access superset api, code: %s. Trying to get published dashboards\n", resp.Status)
} else if resp.StatusCode >= 300 {
logger.Error().Msgf("Failed to access superset api, code: %s. Trying to get published dashboards\n", resp.Status)
return nil, errors.New("failed to access superset api")
}
var dashboardResponse getDashboardResponse
err = json.NewDecoder(resp.Body).Decode(&dashboardResponse)
if err != nil {
return nil, errors.WithStack(err)
}
publishedDashboard = dashboardResponse.Result
return
}
func getDashboardsEmbeddedID(dashboardId int, accToken string) (embeddedId string, err error) {
targetURL, err := url.JoinPath(host, "dashboard/", strconv.Itoa(dashboardId), "embedded")
if err != nil {
return "", errors.WithStack(err)
}
req, err := http.NewRequest("GET", targetURL, nil)
if err != nil {
return "", errors.WithStack(err)
}
req.Header.Set("Accept", "application/json")
req.Header.Add("Authorization", "Bearer "+accToken)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", errors.WithStack(err)
}
defer resp.Body.Close()
if resp.StatusCode == 404 {
logger.Warn().Msgf("Superset dashboard id %s is public, but is not embedable", embeddedId)
return "", errors.New("failed to get embedable id")
} else if resp.StatusCode >= 300 {
logger.Error().Msgf("Failed to access superset api, code: %s. Trying to get embeded dashboard id\n", resp.Status)
return "", errors.New("failed to access superset api")
}
var embeddedResponse getEmbeddedDashboardInfoResponse
err = json.NewDecoder(resp.Body).Decode(&embeddedResponse)
if err != nil {
return "", errors.WithStack(err)
}
return embeddedResponse.Result.UUID, nil
}
type getDashboardResponse struct {
Count int `json:"count"`
Ids []int `json:"ids"`
Result []publishedDashboard `json:"result"`
}
type publishedDashboard struct {
DashboardTitle string `json:"dashboard_title"`
ID int `json:"id"`
}
type getEmbeddedDashboardInfoResponse struct {
Result struct {
UUID string `json:"uuid"`
} `json:"result"`
}
type EmbeddableDashboard struct {
Title string `json:"title"`
EmbeddingId string `json:"embedded_id"`
}

136
pkg/superset/guest_token.go Normal file
View File

@@ -0,0 +1,136 @@
package superset
import (
"bytes"
"encoding/json"
"io/ioutil"
"net/http"
"fiskerinc.com/modules/httpclient"
"fiskerinc.com/modules/redis"
"github.com/pkg/errors"
)
var errUnauthorized = errors.New("superset unauthorized")
type (
guestTokenRequest struct {
User user `json:"user"`
Resources []resource `json:"resources"`
Rls []rule `json:"rls"`
}
user struct {
UserName string `json:"username"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
}
resource struct {
ID string `json:"id"`
Type string `json:"type"`
}
rule struct {
Clause string `json:"clause"`
}
guestTokenResponse struct {
Token string `json:"token"`
}
)
func GetGuestToken(r redis.Client, accToken string) (string, error) {
token, err := getGuestToken(accToken)
if err == nil {
return token, nil
}
if err != nil && !errors.Is(err, errUnauthorized) {
return "", err
}
// if unauthorized
accToken, err = loginFunc(r)
if err != nil {
return "", err
}
return getGuestToken(accToken)
}
func getGuestToken(accToken string) (string, error) {
req, err := getGuestTokenReq(accToken)
if err != nil {
return "", err
}
resp, err := httpclient.Client.Do(req)
if err != nil {
return "", errors.WithStack(err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusUnauthorized {
return "", errors.WithStack(errUnauthorized)
}
if resp.StatusCode != http.StatusOK {
return "", errors.Errorf("superset guest token answered with status: %s", resp.Status)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", errors.WithStack(err)
}
var structResp guestTokenResponse
err = json.Unmarshal(body, &structResp)
if err != nil {
return "", errors.WithStack(err)
}
return structResp.Token, nil
}
func getGuestTokenReq(accToken string) (*http.Request, error) {
body, err := json.Marshal(guestTokenRequest{
User: user{
UserName: guestUserName,
FirstName: guestFirstName,
LastName: guestLastName,
},
Resources: compileResources(accToken),
Rls: []rule{},
})
if err != nil {
return nil, errors.WithStack(err)
}
req, err := http.NewRequest(http.MethodPost, host+"/security/guest_token", bytes.NewReader(body))
if err != nil {
return nil, errors.WithStack(err)
}
req.Header.Add("Authorization", "Bearer "+accToken)
req.Header.Add("Content-Type", "application/json")
return req, nil
}
func compileResources(accToken string) []resource {
var res []resource
dashes, err := GetEmbeddableDashboards(accToken)
dashIDs := make([]string, 0)
if err == nil {
for _, d := range dashes {
dashIDs = append(dashIDs, d.EmbeddingId)
}
}
for _, id := range dashIDs {
if id == "" {
continue
}
res = append(res, resource{
ID: id,
Type: "dashboard",
})
}
return res
}

View File

@@ -0,0 +1,115 @@
package superset_test
import (
"bytes"
"io/ioutil"
"net/http"
"testing"
"fiskerinc.com/modules/httpclient"
"fiskerinc.com/modules/httpclient/mock"
"fiskerinc.com/modules/redis"
"fiskerinc.com/modules/redis/tester"
"fiskerinc.com/modules/superset"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)
const (
validAccToken = "acc_token"
unauthAccToken = "not_authorized_acc_token"
validGuestToken = "valid_guest_token"
)
func TestGetGuestToken(t *testing.T) {
redisMock := tester.NewRedisMock()
tests := map[string]struct {
accToken string
httpClientDoFunc func(req *http.Request) (*http.Response, error)
loginFunc func(r redis.Client) (string, error)
expToken string
expErr error
}{
"success": {
accToken: validAccToken,
httpClientDoFunc: successGuestHttpDo,
expToken: validGuestToken,
},
"success_unauthorized": {
accToken: unauthAccToken,
httpClientDoFunc: successGuestHttpDo,
expToken: validGuestToken,
loginFunc: validLoginFunc,
},
"err_http": {
httpClientDoFunc: errorHttpDo,
expErr: someErr,
},
"unknown_http_error": {
httpClientDoFunc: successGuestHttpDo,
expErr: errors.New("superset guest token answered with status: Internal server error"),
},
"login_failed": {
accToken: unauthAccToken,
httpClientDoFunc: successGuestHttpDo,
loginFunc: invalidLoginFunc,
expErr: someErr,
},
"login_wrong_token": {
accToken: unauthAccToken,
httpClientDoFunc: successGuestHttpDo,
loginFunc: wrongTokenLoginFunc,
expErr: errors.New("superset guest token answered with status: Internal server error"),
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
httpclient.Client = &mock.Client{DoFunc: tt.httpClientDoFunc}
superset.SetLoginFunc(tt.loginFunc)
got, err := superset.GetGuestToken(redisMock, tt.accToken)
if err != nil && tt.expErr != nil {
assert.Equal(t, tt.expErr.Error(), err.Error())
return
}
assert.Equal(t, tt.expErr, err)
assert.Equal(t, tt.expToken, got)
})
}
}
func successGuestHttpDo(req *http.Request) (*http.Response, error) {
accToken := req.Header.Get("Authorization")
switch accToken {
case "Bearer " + validAccToken:
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte(`{"token":"valid_guest_token"}`))),
}, nil
case "Bearer " + unauthAccToken:
return &http.Response{
StatusCode: http.StatusUnauthorized,
Body: ioutil.NopCloser(bytes.NewReader([]byte(``))),
}, nil
default:
return &http.Response{
StatusCode: http.StatusInternalServerError,
Status: "Internal server error",
Body: ioutil.NopCloser(bytes.NewReader([]byte(``))),
}, nil
}
}
func validLoginFunc(r redis.Client) (string, error) {
return validAccToken, nil
}
func invalidLoginFunc(r redis.Client) (string, error) {
return "", someErr
}
func wrongTokenLoginFunc(r redis.Client) (string, error) {
return "", nil
}

87
pkg/superset/login.go Normal file
View File

@@ -0,0 +1,87 @@
package superset
import (
"encoding/json"
"io/ioutil"
"net/http"
"fiskerinc.com/modules/httpclient"
"fiskerinc.com/modules/redis"
"github.com/pkg/errors"
)
var loginFunc = login
type supersetRequest struct {
Username string `json:"username"`
Password string `json:"password"`
Provider string `json:"provider"`
Refresh bool `json:"refresh"`
}
type supersetResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}
func GetAccessToken(r redis.Client) (string, error) {
token, err := getCachedAccToken(r)
if err != nil && !errors.Is(err, errTokenNotFound) {
return "", err
}
if err == nil {
return token, nil
}
token, err = login(r)
if err != nil {
return "", err
}
return token, nil
}
func login(r redis.Client) (string, error) {
headers := http.Header{}
headers.Add("Content-Type", "application/json")
resp, err := httpclient.Post(
host+"/security/login",
supersetRequest{
Username: accUserName,
Password: password,
Provider: "db",
Refresh: true,
},
headers,
)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", errors.Errorf("superset login answered with status: %s", resp.Status)
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", errors.WithStack(err)
}
sresp := supersetResponse{}
err = json.Unmarshal(b, &sresp)
if err != nil {
return "", errors.WithStack(err)
}
err = r.Set(redis.SupersetAccTokenKey, sresp.AccessToken)
if err != nil {
return "", err
}
return sresp.AccessToken, nil
}
// SetLoginFunc must be useful for testing.
func SetLoginFunc(login func(r redis.Client) (string, error)) {
loginFunc = login
}

104
pkg/superset/login_test.go Normal file
View File

@@ -0,0 +1,104 @@
package superset_test
import (
"bytes"
"io/ioutil"
"net/http"
"testing"
"fiskerinc.com/modules/httpclient"
"fiskerinc.com/modules/httpclient/mock"
"fiskerinc.com/modules/redis"
"fiskerinc.com/modules/redis/tester"
"fiskerinc.com/modules/superset"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)
var someErr = errors.New("some err")
func TestGetAccessToken(t *testing.T) {
const validToken = "valid_token"
redisMock := tester.NewRedisMock()
type test struct {
redisGetResults interface{}
httpClientDoFunc func(req *http.Request) (*http.Response, error)
redisErr error
expErr error
expResp string
expSetValues map[string]tester.ExpiringCache
}
tests := map[string]test{
"success_cached": {
redisGetResults: []byte(validToken),
expErr: nil,
expResp: validToken,
expSetValues: map[string]tester.ExpiringCache{},
},
"success": {
httpClientDoFunc: successAccHttpDo,
expErr: nil,
expResp: validToken,
expSetValues: map[string]tester.ExpiringCache{
redis.SupersetAccTokenKey: {
Value: validToken,
},
},
},
"failed_cache_func": {
redisErr: someErr,
expErr: someErr,
},
"invalid_token": {
redisGetResults: 12,
expErr: errors.New("invalid superset access token type (expected []byte]); access token: 12"),
},
"error_request": {
httpClientDoFunc: errorHttpDo,
expErr: someErr,
},
"forbidden_request": {
httpClientDoFunc: forbiddenAccHttpDo,
expErr: errors.New("superset login answered with status: 403 Forbidden"),
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
httpclient.Client = &mock.Client{DoFunc: tt.httpClientDoFunc}
redisMock.Reset()
redisMock.GetResults = tt.redisGetResults
redisMock.Error = tt.redisErr
got, err := superset.GetAccessToken(redisMock)
if err != nil && tt.expErr != nil {
assert.Equal(t, tt.expErr.Error(), err.Error())
return
}
assert.Equal(t, tt.expErr, err)
assert.Equal(t, tt.expResp, got)
assert.Equal(t, tt.expSetValues, redisMock.SetValues)
})
}
}
func successAccHttpDo(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte(`{"access_token":"valid_token","refresh_token":""}`))),
}, nil
}
func errorHttpDo(req *http.Request) (*http.Response, error) {
return nil, someErr
}
func forbiddenAccHttpDo(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusForbidden,
Status: "403 Forbidden",
Body: ioutil.NopCloser(bytes.NewReader([]byte(``))),
}, nil
}

12
pkg/superset/superset.go Normal file
View File

@@ -0,0 +1,12 @@
package superset
import "fiskerinc.com/modules/utils/envtool"
var (
host = envtool.GetEnv("SUPERSET_URL", "REPLACE_ME")
accUserName = envtool.GetEnv("SUPERSET_ACCESS_ACCOUNT_USERNAME", "REPLACE_ME")
password = envtool.GetEnv("SUPERSET_ACCESS_ACCOUNT_PASSWORD", "REPLACE_ME")
guestUserName = envtool.GetEnv("SUPERSET_GUEST_ACCOUNT_USERNAME", "REPLACE_ME")
guestFirstName = envtool.GetEnv("SUPERSET_GUEST_ACCOUNT_FIRSTNAME", "REPLACE_ME")
guestLastName = envtool.GetEnv("SUPERSET_GUEST_ACCOUNT_LASTNAME", "REPLACE_ME")
)