Skip to content

Commit

Permalink
feat: [KLD6-182] store sub in user entity (#1065)
Browse files Browse the repository at this point in the history
* feat: [KDL6-188] store sub from Keycloak into User entity
  • Loading branch information
igzjaviergil authored Dec 4, 2024
1 parent 07d883c commit c4e06cd
Show file tree
Hide file tree
Showing 12 changed files with 170 additions and 48 deletions.
1 change: 1 addition & 0 deletions .github/.golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ issues:
- path: _test\.go
linters:
- mnd
- gochecknoglobals
# https://github.com/go-critic/go-critic/issues/926
- linters:
- gocritic
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ docker-lint: ## Lints Dockerfile defined in dir Example: make docker-lint dir=ap

.PHONY: tidy
tidy: ## Run golangci-lint, goimports and gofmt
golangci-lint run --config .github/.golangci.yml app/api/... cleaner/... repo-cloner/... && goimports -w app/api cleaner repo-cloner && gofmt -s -w -e -d app/api cleaner repo-cloner
golangci-lint run --config .github/.golangci.yml --build-tags=integration,unit app/api/... cleaner/... repo-cloner/... && goimports -w app/api cleaner repo-cloner && gofmt -s -w -e -d app/api cleaner repo-cloner

.PHONY: create
create: ## Creates a complete local environment
Expand Down
19 changes: 13 additions & 6 deletions app/api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,23 @@ go test ./...

## Linters

`golangci-lint` is a fast Go linters runner. It runs linters in parallel, uses caching, supports yaml config, has
integrations with all major IDE and has dozens of linters included.
`golangci-lint` is a fast Go linters runner. It runs linters in parallel, uses
caching, supports yaml config, has integrations with all major IDE and has
dozens of linters included.

As you can see in the `.golangci.yml` config file of this repo, we enable more linters than the default and have more
strict settings.
As you can see in the `.github/.golangci.yml` config file of this repo, we
enable more linters than the default and have more strict settings.

To run `golangci-lint` execute:
To run `golangci-lint` in the repository execute:

```console
make tidy
```
golangci-lint run

To run `golangci-lint` in the app/api folder execute:

```console
golangci-lint run --config ../../.github/.golangci.yml --build-tags=integration,unit
```

## Environment variables
Expand Down
1 change: 1 addition & 0 deletions app/api/entity/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type User struct {
ID string
Email string
Username string
Sub string
Deleted bool
CreationDate time.Time
AccessLevel AccessLevel
Expand Down
13 changes: 3 additions & 10 deletions app/api/http/middleware/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"net/http"

"github.com/konstellation-io/kdl-server/app/api/entity"
"github.com/konstellation-io/kdl-server/app/api/pkg/kdlutil"
"github.com/konstellation-io/kdl-server/app/api/usecase/user"
)

Expand Down Expand Up @@ -39,22 +38,16 @@ Example:
func AuthMiddleware(next http.Handler, userUsecase user.UseCase) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
email := r.Header.Get("X-Forwarded-Email")
username := r.Header.Get("X-Forwarded-User")
sub := r.Header.Get("X-Forwarded-User")

if email == "" || username == "" {
if email == "" || sub == "" {
w.WriteHeader(http.StatusUnauthorized)
return
}

_, err := userUsecase.GetByEmail(r.Context(), email)
if errors.Is(err, entity.ErrUserNotFound) {
// make sure we use extracted username from email
extractedUsername := kdlutil.GetUsernameFromEmail(email)
if extractedUsername != "" {
username = extractedUsername
}

_, err = userUsecase.Create(r.Context(), email, username, entity.AccessLevelViewer)
_, err = userUsecase.Create(r.Context(), email, sub, entity.AccessLevelViewer)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
Expand Down
6 changes: 5 additions & 1 deletion app/api/http/middleware/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ func (ts *AuthMiddlewareTestSuite) TestMiddlewareAuthUsernameNotFound() {
id = "user.1234"
email = "[email protected]"
username = "user"
sub = "d5d70477-5192-4182-b80e-5d34550eb4fe"
accessLevel = entity.AccessLevelViewer
publicSSHKey = "test-ssh-key-public"
privateSSHKey = "test-ssh-key-private"
Expand All @@ -127,6 +128,7 @@ func (ts *AuthMiddlewareTestSuite) TestMiddlewareAuthUsernameNotFound() {
u := entity.User{
Username: username,
Email: email,
Sub: sub,
AccessLevel: accessLevel,
SSHKey: sshKey,
CreationDate: now,
Expand All @@ -135,6 +137,7 @@ func (ts *AuthMiddlewareTestSuite) TestMiddlewareAuthUsernameNotFound() {
ID: id,
Username: username,
Email: email,
Sub: sub,
AccessLevel: accessLevel,
SSHKey: sshKey,
CreationDate: now,
Expand All @@ -143,6 +146,7 @@ func (ts *AuthMiddlewareTestSuite) TestMiddlewareAuthUsernameNotFound() {
ts.mocks.repo.EXPECT().GetByEmail(ctx, email).Return(entity.User{}, entity.ErrUserNotFound)
ts.mocks.repo.EXPECT().GetByUsername(ctx, username).Return(entity.User{}, entity.ErrUserNotFound)
ts.mocks.repo.EXPECT().GetByEmail(ctx, email).Return(entity.User{}, entity.ErrUserNotFound)
ts.mocks.repo.EXPECT().GetBySub(ctx, sub).Return(entity.User{}, entity.ErrUserNotFound)
ts.mocks.clock.EXPECT().Now().Return(now)
ts.mocks.sshGenerator.EXPECT().NewKeys().Return(sshKey, nil)
ts.mocks.repo.EXPECT().Create(ctx, u).Return(id, nil)
Expand All @@ -157,7 +161,7 @@ func (ts *AuthMiddlewareTestSuite) TestMiddlewareAuthUsernameNotFound() {

req := httptest.NewRequest(http.MethodGet, "/", http.NoBody)
req.Header.Set("X-Forwarded-Email", email)
req.Header.Set("X-Forwarded-User", username)
req.Header.Set("X-Forwarded-User", sub)

res := httptest.NewRecorder()

Expand Down
65 changes: 54 additions & 11 deletions app/api/infrastructure/mongodb/mongo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ const (
runtimesCollName = "runtimes"
userActivityCollName = "userActivity"
userCollName = "users"
rootUsername = "root"
rootPassword = "root"
)

var (
Expand Down Expand Up @@ -129,6 +131,7 @@ var userExamples = map[string]entity.User{
ID: primitive.NewObjectID().Hex(),
Email: "email1",
Username: "user1",
Sub: "d5d70477-5192-4182-b80e-5d34550eb4fe",
Deleted: false,
CreationDate: testTimeExample,
AccessLevel: entity.AccessLevelAdmin,
Expand All @@ -142,6 +145,7 @@ var userExamples = map[string]entity.User{
ID: primitive.NewObjectID().Hex(),
Email: "email2",
Username: "user2",
Sub: "4c5c3da6-2847-4a8a-9f68-9532fe559b6d",
Deleted: false,
CreationDate: testTimeExample,
AccessLevel: entity.AccessLevelAdmin,
Expand Down Expand Up @@ -173,10 +177,8 @@ func (s *TestSuite) SetupSuite() {

zapLog, err := zap.NewDevelopment()
s.Require().NoError(err)
logger := zapr.NewLogger(zapLog)

rootUsername := "root"
rootPassword := "root"
logger := zapr.NewLogger(zapLog)

req := testcontainers.ContainerRequest{
Image: "mongo:8.0",
Expand All @@ -200,7 +202,7 @@ func (s *TestSuite) SetupSuite() {
s.Require().NoError(err)

port := p.Int()
uri := fmt.Sprintf("mongodb://%v:%v@%v:%v/", rootUsername, rootPassword, host, port) //NOSONAR not used in secure contexts
uri := fmt.Sprintf("mongodb://%v:%v@%v:%v/", rootUsername, rootPassword, host, port) // NOSONAR not used in secure contexts
mongoClient, err := mongo.Connect(context.Background(), options.Client().ApplyURI(uri))
s.Require().NoError(err)

Expand Down Expand Up @@ -249,7 +251,10 @@ func (s *TestSuite) SetupTest() {
}

func (s *TestSuite) TearDownTest() {
s.mongoClient.Database(dbName).Drop(context.Background())
err := s.mongoClient.Database(dbName).Drop(context.Background())
if err != nil {
fmt.Println("Error dropping database: %w", err)
}
}

func (s *TestSuite) TestCapabilitiesGet_OK() {
Expand Down Expand Up @@ -334,7 +339,7 @@ func (s *TestSuite) TestProjectAddMembers_OK() {
actualProject, err := s.projectRepo.Get(ctx, project.ID)
s.Require().NoError(err)

s.Equal(len(project.Members)+len(members), len(actualProject.Members))
s.Len(actualProject.Members, len(project.Members)+len(members))
}

func (s *TestSuite) TestProjectRemoveMembers_OK() {
Expand All @@ -361,15 +366,15 @@ func (s *TestSuite) TestProjectRemoveMembers_OK() {
actualProject, err := s.projectRepo.Get(ctx, project.ID)
s.Require().NoError(err)

s.Equal(len(project.Members)+len(members), len(actualProject.Members))
s.Len(actualProject.Members, len(project.Members)+len(members))

err = s.projectRepo.RemoveMembers(ctx, project.ID, users)
s.Require().NoError(err)

actualProject, err = s.projectRepo.Get(ctx, project.ID)
s.Require().NoError(err)

s.Equal(len(project.Members), len(actualProject.Members))
s.Len(actualProject.Members, len(project.Members))
}

func (s *TestSuite) TestProjectUpdateMembersAccessLevel_OK() {
Expand All @@ -380,7 +385,7 @@ func (s *TestSuite) TestProjectUpdateMembersAccessLevel_OK() {
actualProject, err := s.projectRepo.Get(ctx, project.ID)
s.Require().NoError(err)

s.Require().True(len(actualProject.Members) >= 1)
s.Require().GreaterOrEqual(len(actualProject.Members), 1)
aGivenMember := actualProject.Members[len(actualProject.Members)-1]

usersToModify := []entity.User{
Expand Down Expand Up @@ -592,6 +597,25 @@ func (s *TestSuite) TestUserGetByEmail_NotFound() {
s.Equal(entity.ErrUserNotFound, err)
}

func (s *TestSuite) TestUserGetBySub_OK() {
ctx := context.Background()

expectedUser := userExamples["user1"]

actualUser, err := s.userRepo.GetBySub(ctx, expectedUser.Sub)
s.Require().NoError(err)

s.Equal(expectedUser, actualUser)
}

func (s *TestSuite) TestUserGetBySub_NotFound() {
ctx := context.Background()

_, err := s.userRepo.GetBySub(ctx, "notfound")
s.Require().Error(err)
s.Equal(entity.ErrUserNotFound, err)
}

func (s *TestSuite) TestUserFindAll_OK() {
ctx := context.Background()

Expand Down Expand Up @@ -731,16 +755,25 @@ func (s *TestSuite) TestUserEnsureIndexes_OK() {
s.Require().NoError(err)

var indexes []string

for cursor.Next(context.Background()) {
var index bson.M
err := cursor.Decode(&index)
s.Require().NoError(err)

indexes = append(indexes, index["name"].(string))
name, ok := index["name"].(string)
if !ok {
continue
}

indexes = append(indexes, name)
}

s.Len(indexes, 1)
s.Contains(indexes, "_id_")
s.NotContains(indexes, "email_1")
s.NotContains(indexes, "username_1")
s.NotContains(indexes, "sub_1")

err = s.userRepo.EnsureIndexes()
s.Require().NoError(err)
Expand All @@ -750,14 +783,24 @@ func (s *TestSuite) TestUserEnsureIndexes_OK() {
cursor, err = indexView.List(context.Background())
s.Require().NoError(err)

indexes = []string{}

for cursor.Next(context.Background()) {
var index bson.M
err := cursor.Decode(&index)
s.Require().NoError(err)

indexes = append(indexes, index["name"].(string))
name, ok := index["name"].(string)
if !ok {
continue
}

indexes = append(indexes, name)
}

s.Len(indexes, 4)
s.Contains(indexes, "_id_")
s.Contains(indexes, "email_1")
s.Contains(indexes, "username_1")
s.Contains(indexes, "sub_1")
}
14 changes: 14 additions & 0 deletions app/api/infrastructure/mongodb/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type userDTO struct {
Deleted bool `bson:"deleted"`
Username string `bson:"username"`
Email string `bson:"email"`
Sub string `bson:"sub"`
CreationDate time.Time `bson:"creation_date"`
AccessLevel string `bson:"access_level"`
PublicSSHKey string `bson:"public_ssh_key"`
Expand Down Expand Up @@ -67,6 +68,12 @@ func (m *UserRepo) EnsureIndexes() error {
Unique: &unique,
},
},
{
Keys: bson.M{"sub": 1},
Options: &options.IndexOptions{
Unique: &unique,
},
},
})

m.logger.Info("Indexes created for collection", "result", result, "collection", userCollName)
Expand Down Expand Up @@ -94,6 +101,11 @@ func (m *UserRepo) GetByEmail(ctx context.Context, email string) (entity.User, e
return m.findOne(ctx, bson.M{"email": email})
}

// GetBySub retrieves the user using their user sub.
func (m *UserRepo) GetBySub(ctx context.Context, sub string) (entity.User, error) {
return m.findOne(ctx, bson.M{"sub": sub})
}

// FindAll retrieves all the existing users.
func (m *UserRepo) FindAll(ctx context.Context, includeDeleted bool) ([]entity.User, error) {
var filter bson.M
Expand Down Expand Up @@ -260,6 +272,7 @@ func (m *UserRepo) entityToDTO(u entity.User) (userDTO, error) {
dto := userDTO{
Username: u.Username,
Email: u.Email,
Sub: u.Sub,
AccessLevel: string(u.AccessLevel),
PrivateSSHKey: u.SSHKey.Private,
PublicSSHKey: u.SSHKey.Public,
Expand All @@ -285,6 +298,7 @@ func (m *UserRepo) dtoToEntity(dto userDTO) entity.User {
ID: dto.ID.Hex(),
Username: dto.Username,
Email: dto.Email,
Sub: dto.Sub,
AccessLevel: entity.AccessLevel(dto.AccessLevel),
CreationDate: dto.CreationDate,
SSHKey: entity.SSHKey{
Expand Down
Loading

0 comments on commit c4e06cd

Please sign in to comment.