Skip to content
This repository has been archived by the owner on Oct 25, 2023. It is now read-only.

feat(warden): added API for wardenTeamList and updateGroupByShieldMetaData #72

Merged
merged 28 commits into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
780a7cb
feat(warden): added API for teamList
Oct 4, 2023
628335d
chore: format
Oct 4, 2023
84f99ab
feat(warden): updated getTeam call
Oct 5, 2023
6a235a1
feat(warden): updated error handling
Oct 5, 2023
f7a73b9
feat(warden): refactor
Oct 5, 2023
f6ee803
chore: format
Oct 5, 2023
bb7415f
chore: formatting
Oct 6, 2023
49dd47d
feat(warden): updateGroupMetaData with warden team
Oct 9, 2023
6c42045
chore: refactor
Oct 9, 2023
6351b57
chore: lint resolved
Oct 9, 2023
2a07820
test(warden): added test for teamList, updateGroup
Oct 9, 2023
a32bc10
test(warden) : more test added
Oct 9, 2023
d345518
feat(warden): test, error refactored
Oct 10, 2023
d3cd79b
chore: format and lint resolved
Oct 10, 2023
d1f60e8
feat(warden): added warden host as a config
Oct 11, 2023
9f3e743
feat(warden) : added warden client
Oct 12, 2023
5b465ef
feat(warden): replaced warden route by iam
Oct 12, 2023
d69a46d
feat(warden): updated tests
Oct 13, 2023
9cc8f7d
Merge branch 'main' into warden-api
Oct 13, 2023
cb35f97
feat(warden): updated test names
Oct 16, 2023
97f59b2
feat(warden): updated unit test timezone
Oct 16, 2023
e885442
feat(warden): added team-name & product-group-name
Oct 16, 2023
d2968d8
feat(warden): route updated
Oct 17, 2023
c284a9e
feat(warden): updated errors
Oct 20, 2023
4677041
feat(warden): error handled
Oct 23, 2023
05abd06
feat(warden): error handling
Oct 23, 2023
7860372
feat(warden): error updated.
Oct 23, 2023
b1830bd
feat(warden): client error handling updated
sudheerpal Oct 23, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,13 @@ generate:
@make format

generate-mocks:
@mockery --srcpkg=buf.build/gen/go/gotocompany/proton/grpc/go/gotocompany/siren/v1beta1/sirenv1beta1grpc --name=SirenServiceClient
@mockery --srcpkg=buf.build/gen/go/gotocompany/proton/grpc/go/gotocompany/shield/v1beta1/shieldv1beta1grpc --name=ShieldServiceClient
@mockery --srcpkg=buf.build/gen/go/gotocompany/proton/grpc/go/gotocompany/optimus/core/v1beta1/corev1beta1grpc --name=JobSpecificationServiceClient
@mockery --srcpkg=buf.build/gen/go/gotocompany/proton/grpc/go/gotocompany/entropy/v1beta1/entropyv1beta1grpc --name=ResourceServiceClient
@mockery --srcpkg=./internal/server/gcs --name=BlobStorageClient
@mockery --srcpkg=./internal/server/gcs --name=BlobObjectClient
@mockery --srcpkg=./internal/server/gcs --name=ObjectIterator
@mockery --with-expecter --srcpkg=buf.build/gen/go/gotocompany/proton/grpc/go/gotocompany/siren/v1beta1/sirenv1beta1grpc --name=SirenServiceClient
@mockery --with-expecter --srcpkg=buf.build/gen/go/gotocompany/proton/grpc/go/gotocompany/shield/v1beta1/shieldv1beta1grpc --name=ShieldServiceClient
@mockery --with-expecter --srcpkg=buf.build/gen/go/gotocompany/proton/grpc/go/gotocompany/optimus/core/v1beta1/corev1beta1grpc --name=JobSpecificationServiceClient
@mockery --with-expecter --srcpkg=buf.build/gen/go/gotocompany/proton/grpc/go/gotocompany/entropy/v1beta1/entropyv1beta1grpc --name=ResourceServiceClient
@mockery --with-expecter --srcpkg=./internal/server/gcs --name=BlobStorageClient
@mockery --with-expecter --srcpkg=./internal/server/gcs --name=BlobObjectClient
@mockery --with-expecter --srcpkg=./internal/server/gcs --name=ObjectIterator

clean: tidy
@echo "Cleaning up build directories..."
Expand Down
2 changes: 2 additions & 0 deletions internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
kubernetesv1 "github.com/goto/dex/internal/server/v1/kubernetes"
optimusv1 "github.com/goto/dex/internal/server/v1/optimus"
projectsv1 "github.com/goto/dex/internal/server/v1/project"
warden "github.com/goto/dex/internal/server/v1/warden"
)

// Serve initialises all the HTTP API routes, starts listening for requests at addr, and blocks until
Expand Down Expand Up @@ -66,6 +67,7 @@ func Serve(ctx context.Context, addr string,
r.Route("/dlq", dlqv1.Routes(entropyClient, gcsClient))
r.Route("/firehoses", firehosev1.Routes(entropyClient, shieldClient, alertSvc, compassClient, odinAddr, stencilAddr))
r.Route("/kubernetes", kubernetesv1.Routes(entropyClient))
r.Route("/warden", warden.Routes(shieldClient, http.DefaultClient))
})

logger.Info("starting server", zap.String("addr", addr))
Expand Down
8 changes: 8 additions & 0 deletions internal/server/v1/warden/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package warden

import "errors"

var (
ErrTeamNotFound = errors.New("email is not registered on warden")
ErrUserNotFound = errors.New("user not authorized")
StewartJingga marked this conversation as resolved.
Show resolved Hide resolved
)
60 changes: 60 additions & 0 deletions internal/server/v1/warden/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package warden

import (
"errors"
"net/http"

"github.com/go-chi/chi/v5"

"github.com/goto/dex/internal/server/utils"
)

type handler struct {
service *Service
}

func NewHandler(service *Service) *handler {
return &handler{service: service}
}

func (h *handler) teamList(w http.ResponseWriter, r *http.Request) {
teamListResp, err := h.service.TeamList(r.Context())

if errors.Is(err, ErrUserNotFound) {
utils.WriteErrMsg(w, http.StatusUnauthorized, ErrUserNotFound.Error())
return
}
if errors.Is(err, ErrTeamNotFound) {
utils.WriteErrMsg(w, http.StatusNotFound, ErrTeamNotFound.Error())
return
}
if err != nil {
utils.WriteErr(w, err)
return
}

utils.WriteJSON(w, http.StatusOK, teamListResp)
}

func (h *handler) updateGroupMetadata(w http.ResponseWriter, r *http.Request) {
groupID := chi.URLParam(r, "group_id")

var body struct {
WardeTeamID string `json:"warden_team_id"`
StewartJingga marked this conversation as resolved.
Show resolved Hide resolved
}
if err := utils.ReadJSON(r, &body); err != nil {
utils.WriteErr(w, err)
return
} else if body.WardeTeamID == "" {
utils.WriteErrMsg(w, http.StatusBadRequest, "missing warden_team_id")
return
}

resShield, err := h.service.UpdateGroupMetadata(r.Context(), groupID, body.WardeTeamID)
if err != nil {
utils.WriteErr(w, err)
return
}

utils.WriteJSON(w, http.StatusOK, resShield)
}
258 changes: 258 additions & 0 deletions internal/server/v1/warden/handler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
package warden

import (
"bytes"
"context"
"io"
"net/http"
"net/http/httptest"
"testing"

shieldv1beta1 "buf.build/gen/go/gotocompany/proton/protocolbuffers/go/gotocompany/shield/v1beta1"
"github.com/go-chi/chi/v5"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"google.golang.org/protobuf/types/known/structpb"

"github.com/goto/dex/internal/server/reqctx"
"github.com/goto/dex/internal/server/v1/warden/mocks"
shareMocks "github.com/goto/dex/mocks"
)

func TestHandler_teamList(t *testing.T) {
t.Run("should return 200 OK", func(t *testing.T) {
doer := mocks.NewDoer(t)
doer.EXPECT().Do(mock.Anything).Return(&http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBufferString(`{
"success": true,
"errors": [],
"data": {
"teams": [
{
"name": "data_fabric",
"created_at": "2023-08-25T03:52:13.548Z",
"updated_at": "2023-08-25T03:52:13.548Z",
"owner_id": 433,
"parent_team_identifier": "2079834a-05c4-420d-bfc8-44b934adea9f",
"identifier": "b5aea046-dab3-4dac-b1ea-e1eef423226b",
"product_group_name": "data_engineering",
"product_group_id": "2079834a-05c4-420d-bfc8-44b934adea9f",
"labels": null,
"short_code": "T394"
}
]
},
"status": "ok"
}`)),
}, nil)

r := chi.NewRouter()
r.Use(reqctx.WithRequestCtx())
r.Route("/dex/warden", Routes(nil, doer))

req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, "/dex/warden/users/me/warden_teams", nil)
assert.NoError(t, err)
req.Header.Add("X-Auth-Email", "[email protected]")

resp := httptest.NewRecorder()
r.ServeHTTP(resp, req)

assert.Equal(t, http.StatusOK, resp.Code)
assert.JSONEq(t, `{ "teams": [
{
"name": "data_fabric",
"created_at": "2023-08-25T03:52:13.548Z",
"updated_at": "2023-08-25T03:52:13.548Z",
"owner_id": 433,
"parent_team_identifier": "2079834a-05c4-420d-bfc8-44b934adea9f",
"identifier": "b5aea046-dab3-4dac-b1ea-e1eef423226b",
"product_group_name": "data_engineering",
"product_group_id": "2079834a-05c4-420d-bfc8-44b934adea9f",
"labels": null,
"short_code": "T394"
}
]}`, resp.Body.String())
})

t.Run("should return 404 when user email is not present in warden", func(t *testing.T) {
doer := mocks.NewDoer(t)
doer.EXPECT().Do(mock.Anything).Return(&http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBufferString(`{
"success": false,
StewartJingga marked this conversation as resolved.
Show resolved Hide resolved
"errors": [],
"data": {
"teams": [
{
"name": "data_fabric",
"created_at": "2023-08-25T03:52:13.548Z",
"updated_at": "2023-08-25T03:52:13.548Z",
"owner_id": 433,
"parent_team_identifier": "2079834a-05c4-420d-bfc8-44b934adea9f",
"identifier": "b5aea046-dab3-4dac-b1ea-e1eef423226b",
"product_group_name": "data_engineering",
"product_group_id": "2079834a-05c4-420d-bfc8-44b934adea9f",
"labels": null,
"short_code": "T394"
}
]
},
"status": "ok"
}`)),
}, nil)

r := chi.NewRouter()
r.Use(reqctx.WithRequestCtx())
r.Route("/dex/warden", Routes(nil, doer))

req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, "/dex/warden/users/me/warden_teams", nil)
assert.NoError(t, err)
req.Header.Add("X-Auth-Email", "[email protected]")

resp := httptest.NewRecorder()
r.ServeHTTP(resp, req)

assert.Equal(t, http.StatusNotFound, resp.Code)
})

t.Run("should return not authorised when X-Auth-Email is not present is header", func(t *testing.T) {
doer := mocks.NewDoer(t)

r := chi.NewRouter()
r.Use(reqctx.WithRequestCtx())
r.Route("/dex/warden", Routes(nil, doer))

req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, "/dex/warden/users/me/warden_teams", nil)
assert.NoError(t, err)

resp := httptest.NewRecorder()
r.ServeHTTP(resp, req)

assert.Equal(t, http.StatusUnauthorized, resp.Code)
})
}

func TestHandler_updateGroup(t *testing.T) {
t.Run("should return 200 OK", func(t *testing.T) {
groupID := "e38527ee-a8cd-40f9-98a7-1f0bbd20909f"
metaData, _ := structpb.NewStruct(map[string]any{
"privacy": "public",
})

updatedMetaData, _ := structpb.NewStruct(map[string]any{
"privacy": "public",
"team-id": "b5aea046-dab3-4dac-b1ea-e1eef423226b",
"product-group-id": "2079834a-05c4-420d-bfc8-44b934adea9f",
})

shieldClient := shareMocks.NewShieldServiceClient(t)
shieldClient.EXPECT().GetGroup(mock.Anything, &shieldv1beta1.GetGroupRequest{
Id: groupID,
}).Return(&shieldv1beta1.GetGroupResponse{
Group: &shieldv1beta1.Group{
Id: groupID, Name: "test", Slug: "testSlug", OrgId: "123", Metadata: metaData,
},
}, nil)

shieldClient.EXPECT().UpdateGroup(mock.Anything, &shieldv1beta1.UpdateGroupRequest{
Id: groupID, Body: &shieldv1beta1.GroupRequestBody{
Metadata: updatedMetaData, Name: "test", Slug: "testSlug", OrgId: "123",
},
}).Return(&shieldv1beta1.UpdateGroupResponse{Group: &shieldv1beta1.Group{
Id: groupID, Name: "test", Slug: "testSlug", OrgId: "123", Metadata: updatedMetaData,
}}, nil)

doer := mocks.NewDoer(t)
doer.EXPECT().Do(mock.Anything).Return(&http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBufferString(`{
"success": true,
"data": {
"name": "data_fabric",
"created_at": "2023-08-25T03:52:13.548Z",
"updated_at": "2023-08-25T03:52:13.548Z",
"owner_id": 433,
"parent_team_identifier": "2079834a-05c4-420d-bfc8-44b934adea9f",
"identifier": "b5aea046-dab3-4dac-b1ea-e1eef423226b",
"product_group_name": "data_engineering",
"product_group_id": "2079834a-05c4-420d-bfc8-44b934adea9f",
"labels": null,
"short_code": "T394"
},
"errors": []
}`)),
}, nil)

r := chi.NewRouter()
r.Use(reqctx.WithRequestCtx())
r.Route("/dex/warden", Routes(shieldClient, doer))

req, err := http.NewRequestWithContext(context.TODO(), http.MethodPatch, "/dex/warden/groups/e38527ee-a8cd-40f9-98a7-1f0bbd20909f/metadata", bytes.NewBufferString(`{"warden_team_id": "123"}`))
assert.NoError(t, err)

resp := httptest.NewRecorder()
r.ServeHTTP(resp, req)

assert.Equal(t, http.StatusOK, resp.Code)
assert.JSONEq(t, `{
"privacy": "public",
"product-group-id": "2079834a-05c4-420d-bfc8-44b934adea9f",
"team-id": "b5aea046-dab3-4dac-b1ea-e1eef423226b"
}`, resp.Body.String())
})

t.Run("should return 500 when warden_team_id is invalid", func(t *testing.T) {
shieldClient := shareMocks.NewShieldServiceClient(t)

doer := mocks.NewDoer(t)
doer.EXPECT().Do(mock.Anything).Return(&http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBufferString(`{
"success": false,
"data": {
"name": "data_fabric",
"created_at": "2023-08-25T03:52:13.548Z",
"updated_at": "2023-08-25T03:52:13.548Z",
"owner_id": 433,
"parent_team_identifier": "2079834a-05c4-420d-bfc8-44b934adea9f",
"identifier": "b5aea046-dab3-4dac-b1ea-e1eef423226b",
"product_group_name": "data_engineering",
"product_group_id": "2079834a-05c4-420d-bfc8-44b934adea9f",
"labels": null,
"short_code": "T394"
},
"errors": []
}`)),
}, nil)

r := chi.NewRouter()
r.Use(reqctx.WithRequestCtx())
r.Route("/dex/warden", Routes(shieldClient, doer))

req, err := http.NewRequestWithContext(context.TODO(), http.MethodPatch, "/dex/warden/groups/e38527ee-a8cd-40f9-98a7-1f0bbd20909f/metadata", bytes.NewBufferString(`{"warden_team_id": "123"}`))
assert.NoError(t, err)

resp := httptest.NewRecorder()
r.ServeHTTP(resp, req)

assert.Equal(t, http.StatusInternalServerError, resp.Code)
})

t.Run("should return error when warden_team_id is empty string", func(t *testing.T) {
shieldClient := shareMocks.NewShieldServiceClient(t)
doer := mocks.NewDoer(t)

r := chi.NewRouter()
r.Use(reqctx.WithRequestCtx())
r.Route("/dex/warden", Routes(shieldClient, doer))
StewartJingga marked this conversation as resolved.
Show resolved Hide resolved

req, err := http.NewRequestWithContext(context.TODO(), http.MethodPatch, "/dex/warden/groups/e38527ee-a8cd-40f9-98a7-1f0bbd20909f/metadata", bytes.NewBufferString(`{"warden_team_id": ""}`))
assert.NoError(t, err)

resp := httptest.NewRecorder()
r.ServeHTTP(resp, req)

assert.Equal(t, http.StatusBadRequest, resp.Code)
})
}
Loading
Loading