Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Added enhancement in authentication server #4024

Merged
merged 10 commits into from
Jun 29, 2023
28 changes: 28 additions & 0 deletions chaoscenter/authentication/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# BUILD STAGE
FROM golang:1.18 AS builder

ARG TARGETOS=linux
ARG TARGETARCH

ADD . /auth-server
WORKDIR /auth-server

ENV GOOS=${TARGETOS} \
GOARCH=${TARGETARCH}

RUN go env

RUN CGO_ENABLED=0 go build -o /output/server -v ./api/

# Packaging stage
# Image source: https://github.com/litmuschaos/test-tools/blob/master/custom/hardened-alpine/infra/Dockerfile
# The base image is non-root (have litmus user) with default litmus directory.
FROM litmuschaos/infra-alpine

LABEL maintainer="LitmusChaos"

COPY --from=builder /output/server /litmus

CMD ["./server"]

EXPOSE 3000
103 changes: 103 additions & 0 deletions chaoscenter/authentication/api/handlers/grpc/grpc_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package grpc

import (
"context"
"litmus/litmus-portal/authentication/api/middleware"
"litmus/litmus-portal/authentication/api/presenter/protos"
"litmus/litmus-portal/authentication/pkg/entities"
"litmus/litmus-portal/authentication/pkg/validations"
"strconv"

log "github.com/sirupsen/logrus"

"github.com/golang-jwt/jwt"
)

func (s *ServerGrpc) ValidateRequest(ctx context.Context,
inputRequest *protos.ValidationRequest) (*protos.ValidationResponse, error) {
token, err := middleware.ValidateToken(inputRequest.Jwt)
if err != nil {
return &protos.ValidationResponse{Error: err.Error(), IsValid: false}, err
}
claims := token.Claims.(jwt.MapClaims)
uid := claims["uid"].(string)
err = validations.RbacValidator(uid, inputRequest.ProjectId,
inputRequest.RequiredRoles, inputRequest.Invitation, s.ApplicationService)
if err != nil {
return &protos.ValidationResponse{Error: err.Error(), IsValid: false}, err
}
return &protos.ValidationResponse{Error: "", IsValid: true}, nil
}

func (s *ServerGrpc) GetProjectById(ctx context.Context,
inputRequest *protos.GetProjectByIdRequest) (*protos.GetProjectByIdResponse, error) {

project, err := s.ApplicationService.GetProjectByProjectID(inputRequest.ProjectID)
if err != nil {
log.Error(err)
return nil, err
}

// Fetching user ids of all the members in the project
var uids []string

for _, member := range project.Members {
uids = append(uids, member.UserID)
}

memberMap := make(map[string]entities.User)

authUsers, err := s.ApplicationService.FindUsersByUID(uids)
for _, authUser := range *authUsers {
memberMap[authUser.ID] = authUser
}

var projectMembers []*protos.ProjectMembers

// Adding additional details of project members
for _, member := range project.Members {
var projectMember protos.ProjectMembers
projectMember.Email = memberMap[member.UserID].Email
projectMember.UserName = memberMap[member.UserID].UserName
projectMember.Invitation = string(member.Invitation)
projectMember.Uid = member.UserID
projectMember.JoinedAt = member.JoinedAt
projectMembers = append(projectMembers, &projectMember)
}

if err != nil {
return nil, err
}

return &protos.GetProjectByIdResponse{
Id: project.ID,
Name: project.Name,
Members: projectMembers,
State: "",
CreatedAt: strconv.FormatInt(project.CreatedAt, 10),
UpdatedAt: strconv.FormatInt(project.UpdatedAt, 10),
}, nil
}

func (s *ServerGrpc) GetUserById(ctx context.Context,
inputRequest *protos.GetUserByIdRequest) (*protos.GetUserByIdResponse, error) {
user, err := s.ApplicationService.GetUser(inputRequest.UserID)
if err != nil {
log.Error(err)
return nil, err
}
var deactivatedAt string
if user.DeactivatedAt != nil {
deactivatedAt = strconv.FormatInt(*user.DeactivatedAt, 10)
}
return &protos.GetUserByIdResponse{
Id: user.ID,
Name: user.Name,
Username: user.UserName,
CreatedAt: strconv.FormatInt(user.CreatedAt, 10),
UpdatedAt: strconv.FormatInt(user.UpdatedAt, 10),
DeactivatedAt: deactivatedAt,
Role: string(user.Role),
Email: user.Email,
}, nil
}
11 changes: 11 additions & 0 deletions chaoscenter/authentication/api/handlers/grpc/grpc_server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package grpc

import (
"litmus/litmus-portal/authentication/api/presenter/protos"
"litmus/litmus-portal/authentication/pkg/services"
)

type ServerGrpc struct {
services.ApplicationService
protos.UnimplementedAuthRpcServiceServer
}
126 changes: 126 additions & 0 deletions chaoscenter/authentication/api/handlers/rest/dex_auth_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package rest

import (
"context"
"litmus/litmus-portal/authentication/api/presenter"
"litmus/litmus-portal/authentication/pkg/entities"
"litmus/litmus-portal/authentication/pkg/services"
"litmus/litmus-portal/authentication/pkg/utils"
"net/http"
"time"

"github.com/coreos/go-oidc/v3/oidc"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"golang.org/x/oauth2"
)

func oAuthDexConfig() (*oauth2.Config, *oidc.IDTokenVerifier, error) {
ctx := oidc.ClientContext(context.Background(), &http.Client{})
provider, err := oidc.NewProvider(ctx, utils.DexOIDCIssuer)
if err != nil {
log.Errorf("OAuth Error: Something went wrong with OIDC provider %s", err)
return nil, nil, err
}
return &oauth2.Config{
RedirectURL: utils.DexCallBackURL,
ClientID: utils.DexClientID,
ClientSecret: utils.DexClientSecret,
Scopes: []string{"openid", "profile", "email"},
Endpoint: provider.Endpoint(),
}, provider.Verifier(&oidc.Config{ClientID: utils.DexClientID}), nil
}

// DexLogin handles and redirects to DexServer to proceed with OAuth
func DexLogin() gin.HandlerFunc {
return func(c *gin.Context) {

dexToken, err := utils.GenerateOAuthJWT()
if err != nil {
log.Error(err)
c.JSON(utils.ErrorStatusCodes[utils.ErrServerError], presenter.CreateErrorResponse(utils.ErrServerError))
return
}
config, _, err := oAuthDexConfig()
if err != nil {
log.Error(err)
c.JSON(utils.ErrorStatusCodes[utils.ErrServerError], presenter.CreateErrorResponse(utils.ErrServerError))
return
}
url := config.AuthCodeURL(dexToken)
c.Redirect(http.StatusTemporaryRedirect, url)
}
}

// DexCallback is the handler that creates/logs in the user from Dex and provides JWT to frontend via a redirect
func DexCallback(userService services.ApplicationService) gin.HandlerFunc {
return func(c *gin.Context) {
incomingState := c.Query("state")
validated, err := utils.ValidateOAuthJWT(incomingState)
if !validated {
c.Redirect(http.StatusTemporaryRedirect, "/")
}
config, verifier, err := oAuthDexConfig()
if err != nil {
log.Error(err)
c.JSON(utils.ErrorStatusCodes[utils.ErrServerError], presenter.CreateErrorResponse(utils.ErrServerError))
return
}
token, err := config.Exchange(context.Background(), c.Query("code"))
if err != nil {
log.Error(err)
c.JSON(utils.ErrorStatusCodes[utils.ErrServerError], presenter.CreateErrorResponse(utils.ErrServerError))
return
}

rawIDToken, ok := token.Extra("id_token").(string)
if !ok {
log.Error("OAuth Error: no raw id_token found")
c.JSON(utils.ErrorStatusCodes[utils.ErrServerError], presenter.CreateErrorResponse(utils.ErrServerError))
return
}
idToken, err := verifier.Verify(c, rawIDToken)
if err != nil {
log.Error("OAuth Error: no id_token found")
c.JSON(utils.ErrorStatusCodes[utils.ErrServerError], presenter.CreateErrorResponse(utils.ErrServerError))
return
}

var claims struct {
Name string
Email string `json:"email"`
Verified bool `json:"email_verified"`
}
if err := idToken.Claims(&claims); err != nil {
log.Error("OAuth Error: claims not found")
c.JSON(utils.ErrorStatusCodes[utils.ErrServerError], presenter.CreateErrorResponse(utils.ErrServerError))
return
}
createdAt := time.Now().Unix()

var userData = entities.User{
Name: claims.Name,
Email: claims.Email,
UserName: claims.Email,
Role: entities.RoleUser,
Audit: entities.Audit{
CreatedAt: createdAt,
UpdatedAt: createdAt,
},
}

signedInUser, err := userService.LoginUser(&userData)
if err != nil {
log.Error(err)
c.JSON(utils.ErrorStatusCodes[utils.ErrServerError], presenter.CreateErrorResponse(utils.ErrServerError))
return
}
jwtToken, err := signedInUser.GetSignedJWT()
if err != nil {
log.Error(err)
c.JSON(utils.ErrorStatusCodes[utils.ErrServerError], presenter.CreateErrorResponse(utils.ErrServerError))
return
}
c.Redirect(http.StatusPermanentRedirect, "/login?jwtToken="+jwtToken)
}
}
71 changes: 71 additions & 0 deletions chaoscenter/authentication/api/handlers/rest/misc_handlers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package rest

import (
"litmus/litmus-portal/authentication/pkg/entities"
"litmus/litmus-portal/authentication/pkg/services"

"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
)

func contains(s []string, str string) bool {
for _, v := range s {
if v == str {
return true
}
}

return false
}

type ReadinessAPIStatus struct {
DataBase string `json:"database"`
Collections string `json:"collections"`
}

// Status will request users list and return, if successful,
// an http code 200
func Status(service services.ApplicationService) gin.HandlerFunc {
return func(c *gin.Context) {
_, err := service.GetUsers()
if err != nil {
log.Error(err)
c.JSON(500, entities.APIStatus{"down"})
return
}
c.JSON(200, entities.APIStatus{"up"})
}
}

func Readiness(service services.ApplicationService) gin.HandlerFunc {
return func(c *gin.Context) {
var (
db_flag = "up"
col_flag = "up"
)

dbs, err := service.ListDataBase()
if !contains(dbs, "auth") {
db_flag = "down"
}

if err != nil {
log.Error(err)
c.JSON(500, ReadinessAPIStatus{"down", "unknown"})
return
}

cols, err := service.ListCollection()
if !contains(cols, "project") || !contains(cols, "users") {
col_flag = "down"
}

if err != nil {
log.Error(err)
c.JSON(500, ReadinessAPIStatus{db_flag, "down"})
return
}

c.JSON(200, ReadinessAPIStatus{db_flag, col_flag})
}
}