diff --git a/configs/application.yml b/configs/application.yml index 42c5b57..b56e58c 100644 --- a/configs/application.yml +++ b/configs/application.yml @@ -73,8 +73,11 @@ elastic: index: cybertron api_key: - - aws: region: ap-south-1 bucket: navi-cd955a63c4476df0f00c1cea0e4a40d1 + +#mjolnir config +mjolnir: + service.url: https://qa-mjolnir-service.np.navi-ppl.in + realm.id: O3G7sCWk4r diff --git a/configs/config.go b/configs/config.go index 33e4b16..41f6750 100644 --- a/configs/config.go +++ b/configs/config.go @@ -21,6 +21,7 @@ type AppConfig struct { awsConfig *AwsConfig KafkaConfig *KafkaConfig ElasticConfig *ElasticConfig + mjolnir *MjolnirClientConfig } type MigConfig struct { @@ -47,6 +48,7 @@ func LoadConfig() { awsConfig: NewAWSConfig(), KafkaConfig: NewKafkaConfig(), ElasticConfig: NewElasticConfig(), + mjolnir: NewMjolnirConfig(), } } @@ -112,3 +114,7 @@ func GetKafkaConfig() *KafkaConfig { func GetElasticConfig() *ElasticConfig { return appConfig.ElasticConfig } + +func GetMjolnirConfig() *MjolnirClientConfig { + return appConfig.mjolnir +} diff --git a/configs/mjolnir_config.go b/configs/mjolnir_config.go new file mode 100644 index 0000000..f5887c6 --- /dev/null +++ b/configs/mjolnir_config.go @@ -0,0 +1,22 @@ +package configs + +type MjolnirClientConfig struct { + baseUrl string + realmId string +} + +func NewMjolnirConfig() *MjolnirClientConfig { + + return &MjolnirClientConfig{ + baseUrl: getString("mjolnir.service.url", true), + realmId: getString("mjolnir.realm.id", true), + } +} + +func (p *MjolnirClientConfig) GetMjolnirBaseUrl() string { + return p.baseUrl +} + +func (p *MjolnirClientConfig) GetMjolnirRealmId() string { + return p.realmId +} diff --git a/constants/common.go b/constants/common.go new file mode 100644 index 0000000..2ae262e --- /dev/null +++ b/constants/common.go @@ -0,0 +1,6 @@ +package constants + +const ( + SESSION_HEADER_NAME = "x-session-token" + EMAIL_HEADER_NAME = "x-user-email" +) diff --git a/internal/dependencies/dependencies.go b/internal/dependencies/dependencies.go index 0af925c..624bcce 100644 --- a/internal/dependencies/dependencies.go +++ b/internal/dependencies/dependencies.go @@ -11,6 +11,7 @@ import ( httpclient "cybertron/pkg/httpClient" "cybertron/pkg/kafka/producer" "cybertron/pkg/log" + "cybertron/pkg/mjolnirClient" "cybertron/service" "go.uber.org/zap" @@ -31,6 +32,7 @@ type Service struct { SourceMapService *service.SourceMapService ReleaseService *service.ReleaseService ExceptionService *service.ExceptionService + AuthService *service.AuthService SearchService *service.SearchService S3Client *aws.Actions // Add your service here @@ -58,6 +60,7 @@ func InitDependencies() *Dependencies { s3Client := aws.NewS3client(*configs.GetAWSConfig()) kafkaProducer := initKafkaProducer() elasticSearch, _ := elastic.NewElasticClient(*configs.GetElasticConfig()) + mjolnirClient := mjolnirClient.NewMjolnirClient(*configs.GetMjolnirConfig()) documentServiceClient := document.NewDocumentServiceHttpClient(httpClient, logger, configs.GetDocumentServiceHttpClientConfigs()) projectServiceClient := service.NewProjectCreator(logger, dbClient, s3Client, kafkaProducer) @@ -65,9 +68,11 @@ func InitDependencies() *Dependencies { releaseServiceClient := service.NewReleaseService(logger, dbClient) exceptionServiceClient := service.NewExceptionService(logger, dbClient, kafkaProducer) searchServiceClient := service.NewSearchService(logger, elasticSearch) + authService := service.NewAuthService(mjolnirClient) - services := initServices(documentServiceClient, projectServiceClient, sourceMapServiceClient, releaseServiceClient, exceptionServiceClient, searchServiceClient) + services := initServices(documentServiceClient, projectServiceClient, sourceMapServiceClient, releaseServiceClient, exceptionServiceClient, searchServiceClient, authService) handlers := initHandlers(projectServiceClient, sourceMapServiceClient, releaseServiceClient, exceptionServiceClient, searchServiceClient) + return &Dependencies{ Service: services, DBClient: dbClient, @@ -77,7 +82,7 @@ func InitDependencies() *Dependencies { } } -func initServices(documentService *document.HttpClient, projectService *service.ProjectCreator, sourceMapService *service.SourceMapService, releaseService *service.ReleaseService, exceptionService *service.ExceptionService, searchService *service.SearchService) *Service { +func initServices(documentService *document.HttpClient, projectService *service.ProjectCreator, sourceMapService *service.SourceMapService, releaseService *service.ReleaseService, exceptionService *service.ExceptionService, searchService *service.SearchService, authService *service.AuthService) *Service { return &Service{ DocumentService: documentService, ProjectService: projectService, @@ -85,6 +90,7 @@ func initServices(documentService *document.HttpClient, projectService *service. ReleaseService: releaseService, ExceptionService: exceptionService, SearchService: searchService, + AuthService: authService, } } diff --git a/internal/transport/middleware/permission_middleware.go b/internal/transport/middleware/permission_middleware.go new file mode 100644 index 0000000..e8a2d3f --- /dev/null +++ b/internal/transport/middleware/permission_middleware.go @@ -0,0 +1,52 @@ +package middleware + +import ( + "cybertron/constants" + "cybertron/service" + "net/http" + + "github.com/gin-gonic/gin" +) + +type UserInfo struct { + SessionToken string `json:"sessionToken"` + ClientID string `json:"clientId"` + Name string `json:"name"` + Exp int `json:"exp"` + EmailID string `json:"emailId"` + AccountID string `json:"accountId"` + PhoneNumber string `json:"phoneNumber"` + Roles []string `json:"roles"` + Groups []string `json:"groups"` + Permissions []string `json:"permissions"` + FirebaseJwtToken string `json:"firebaseJwtToken"` + FirebaseNode string `json:"firebaseNode"` + ProfilePictureURL string `json:"profilePictureUrl"` + PreferredUsername string `json:"preferred_username"` +} + +func PermissionMiddleware(authService *service.AuthService) gin.HandlerFunc { + return func(c *gin.Context) { + + sessionToken := c.GetHeader(constants.SESSION_HEADER_NAME) + userEmail := c.GetHeader(constants.EMAIL_HEADER_NAME) + + validUser, err := authService.CheckValidUser(c, sessionToken, userEmail) + if err != nil || !validUser { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + c.Abort() + return + } + + c.Next() + } +} + +func isAdmin(roles []string) bool { + for _, role := range roles { + if role == "Admin" { + return true + } + } + return false +} diff --git a/internal/transport/server.go b/internal/transport/server.go index 84c80f9..6b87d50 100644 --- a/internal/transport/server.go +++ b/internal/transport/server.go @@ -1,6 +1,7 @@ package transport import ( + "cybertron/internal/transport/middleware" "cybertron/internal/transport/router" "fmt" "github.com/gin-contrib/cors" @@ -48,6 +49,7 @@ func (s *Server) Start() { AllowCredentials: true, MaxAge: 24 * time.Hour, })) + s.gin.Use(middleware.PermissionMiddleware(s.dependencies.Service.AuthService)) s.router() port := configs.GetPort() diff --git a/models/db/project.go b/models/db/project.go index 1823dcc..fe04ad3 100644 --- a/models/db/project.go +++ b/models/db/project.go @@ -5,7 +5,7 @@ import ( ) type Project struct { - ID int `json:"id"` + ID int `json:"id" gorm:"autoIncrement:true"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` DeletedAt time.Time `json:"deletedAt"` diff --git a/models/db/release.go b/models/db/release.go index 43eab6d..b02f6e0 100644 --- a/models/db/release.go +++ b/models/db/release.go @@ -1,9 +1,14 @@ package db -import "gorm.io/gorm" +import ( + "time" +) type Release struct { - gorm.Model - ProjectReferenceId string `gorm:"primaryKey"` - ReleaseVersion string `gorm:"column:name"` + ID int64 `json:"id" gorm:"autoIncrement:true"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + DeletedAt time.Time `json:"deletedAt"` + ProjectReferenceId string `json:"projectReferenceId" gorm:"primaryKey"` + ReleaseVersion string `json:"releaseVersion" gorm:"primaryKey"` } diff --git a/pkg/mjolnirClient/mjolnirClient.go b/pkg/mjolnirClient/mjolnirClient.go new file mode 100644 index 0000000..aee186b --- /dev/null +++ b/pkg/mjolnirClient/mjolnirClient.go @@ -0,0 +1,78 @@ +package mjolnirClient + +import ( + "cybertron/configs" + "cybertron/pkg/log" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" +) + +type MjolnirClient struct { + baseUrl string + realmId string +} + +type MjolnirError struct { + Code string `json:"code,omitempty"` + Message string `json:"message,omitempty"` + Params interface{} `json:"params,omitempty"` +} + +type MjolnirSessionResponse struct { + SessionToken string `json:"sessionToken,omitempty"` + ClientId string `json:"clientId,omitempty"` + EmailId string `json:"emailId,omitempty"` + AccountId string `json:"accountId,omitempty"` + PhoneNumber string `json:"phoneNumber,omitempty"` + PreferredUsername string `json:"preferred_username,omitempty"` + Roles []string `json:"roles,omitempty"` + Groups []string `json:"groups,omitempty"` + Permissions []string `json:"permissions,omitempty"` + StatusCode int `json:"statusCode,omitempty"` + Errors []MjolnirError `json:"errors"` +} + +func NewMjolnirClient(mjolnirConfig configs.MjolnirClientConfig) *MjolnirClient { + return &MjolnirClient{ + baseUrl: mjolnirConfig.GetMjolnirBaseUrl(), + realmId: mjolnirConfig.GetMjolnirRealmId(), + } +} + +var logger = log.Log.GetLog() +var client = http.Client{} + +const ( + SessionUrl = "%s/session/%s" +) + +func (m *MjolnirClient) GetSessionResponse(sessionToken string) (*MjolnirSessionResponse, error) { + if sessionToken == "null" { + return nil, errors.New("unauthorized request") + } + + req, _ := http.NewRequest("GET", fmt.Sprintf(SessionUrl, m.baseUrl, m.realmId), nil) + req.Header.Add("X-Session-Token", sessionToken) + resp, err := client.Do(req) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + responseBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var response MjolnirSessionResponse + err = json.Unmarshal(responseBody, &response) + if err != nil { + return nil, err + } + logger.Info(fmt.Sprintf("%v", response)) + + return &response, nil +} diff --git a/service/AuthService.go b/service/AuthService.go new file mode 100644 index 0000000..d1d218a --- /dev/null +++ b/service/AuthService.go @@ -0,0 +1,34 @@ +package service + +import ( + "cybertron/pkg/mjolnirClient" + "fmt" + "github.com/gin-gonic/gin" + "net/http" +) + +type AuthService struct { + mjolnirClient *mjolnirClient.MjolnirClient +} + +func NewAuthService(mjolnirClient *mjolnirClient.MjolnirClient) *AuthService { + return &AuthService{ + mjolnirClient: mjolnirClient, + } +} + +type UserRoles string + +func (authService *AuthService) CheckValidUser(c *gin.Context, sessionToken, userEmail string) (bool, error) { + sessionResponse, err := authService.mjolnirClient.GetSessionResponse(sessionToken) + if err != nil || sessionResponse.StatusCode == http.StatusUnauthorized { + return false, err + } + if sessionResponse.EmailId != userEmail { + return false, fmt.Errorf("user email: %v does not match the email linked to the session token", userEmail) + } + + c.Set("permissions", sessionResponse.Permissions) + + return true, nil +} diff --git a/service/ReleaseService.go b/service/ReleaseService.go index de0cec7..c5085df 100644 --- a/service/ReleaseService.go +++ b/service/ReleaseService.go @@ -14,9 +14,8 @@ type ReleaseService struct { } type ReleaseBody struct { - ProjectId string `json:"project_id" binding:"required"` - Version string `json:"version" binding:"required"` - SourceMapUrl string `json:"source_map_url" binding:"required"` + ProjectReferenceId string `json:"projectReferenceId" binding:"required"` + ReleaseVersion string `json:"releaseVersion" binding:"required"` } func NewReleaseService(logger *log.Logger, dbClient *gorm.DB) *ReleaseService { @@ -26,7 +25,6 @@ func NewReleaseService(logger *log.Logger, dbClient *gorm.DB) *ReleaseService { } } -// FIXME: This may not be requried now, can be used in source maps services only func (releaseService *ReleaseService) AddRelease(c *gin.Context) { var releaseBody ReleaseBody if err := c.BindJSON(&releaseBody); err != nil { @@ -37,8 +35,8 @@ func (releaseService *ReleaseService) AddRelease(c *gin.Context) { } releaseToBeAdded := db.Release{ - ProjectReferenceId: releaseBody.ProjectId, - ReleaseVersion: releaseBody.Version, + ProjectReferenceId: releaseBody.ProjectReferenceId, + ReleaseVersion: releaseBody.ReleaseVersion, } if result := releaseService.dbClient.Create(&releaseToBeAdded); result.Error != nil { @@ -53,11 +51,12 @@ func (releaseService *ReleaseService) AddRelease(c *gin.Context) { func (releaseService *ReleaseService) GetReleases(c *gin.Context) { projectRefId := c.Query("project_reference_id") + releaseVersion := c.Query("release_version") var releases []db.Release - releaseService.dbClient.Where("project_reference_id = ?", projectRefId).Find(&releases) + result := releaseService.dbClient.Where("project_reference_id = ? OR release_version = ?", projectRefId, releaseVersion).Find(&releases) - if result := releaseService.dbClient.Find(&releases); result.Error != nil { + if result.Error != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()}) return } diff --git a/service/sourceMap.go b/service/sourceMap.go index 7b7c13b..e3d2efc 100644 --- a/service/sourceMap.go +++ b/service/sourceMap.go @@ -21,9 +21,9 @@ type SourceMapService struct { } type SourceMapAckBody struct { - ProjectId string `json:"project-id" binding:"required"` - ReleaseId string `json:"releaseId" binding:"required"` - FileName string `json:"file_name" binding:"required"` + ProjectReferenceId string `json:"projectId" binding:"required"` + ReleaseId string `json:"releaseId" binding:"required"` + FileName string `json:"fileName" binding:"required"` } func NewSourceMapService(dbClient *gorm.DB, s3Client *aws.Actions, config *configs.AwsConfig) *SourceMapService { @@ -45,10 +45,11 @@ func (s *SourceMapService) GetSourceMapUploadUrl(ctx *gin.Context) { }) return } + //generate s3 pre-signed url key := path.Join(projectId, releaseId, fileName) bucket := s.awsConfig.Bucket - request, err := s.s3Client.S3PresignClient.PresignGetObject(context.TODO(), &s3.GetObjectInput{ + request, err := s.s3Client.S3PresignClient.PresignPutObject(context.TODO(), &s3.PutObjectInput{ Bucket: &bucket, Key: &key, }, func(opts *s3.PresignOptions) { @@ -83,9 +84,10 @@ func (s *SourceMapService) SourceMapUploadAck(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } + //find the record to update ack var existingSourceMap db.SourceMap - existingRecordError := s.dbClient.First(&existingSourceMap, "project_reference_id = ? and release_reference_id= ? and file_name = ?", sourceMapAckBody.ProjectId, sourceMapAckBody.ReleaseId, sourceMapAckBody.FileName).Error + existingRecordError := s.dbClient.First(&existingSourceMap, "project_reference_id = ? and release_reference_id= ? and file_name = ?", sourceMapAckBody.ProjectReferenceId, sourceMapAckBody.ReleaseId, sourceMapAckBody.FileName).Error if existingRecordError != nil { c.JSON(http.StatusBadRequest, gin.H{"error": existingRecordError.Error()}) return