package utils
import (
"fmt"
"time"
"github.com/golang-jwt/jwt/v4"
"github.com/sirupsen/logrus"
)
func GenerateOAuthJWT() (string, error) {
token := jwt.New(jwt.SigningMethodHS512)
claims := token.Claims.(jwt.MapClaims)
claims["exp"] = time.Now().Add(time.Minute * time.Duration(OAuthJWTExpDuration)).Unix()
tokenString, err := token.SignedString([]byte(OAuthJwtSecret))
if err != nil {
logrus.Info(err)
return "", err
}
return tokenString, nil
}
func ValidateOAuthJWT(tokenString string) (bool, error) {
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, isValid := token.Method.(*jwt.SigningMethodHMAC); !isValid {
return nil, fmt.Errorf("invalid token %s", token.Header["alg"])
}
return []byte(OAuthJwtSecret), nil
})
if err != nil {
return false, err
}
if _, ok := token.Claims.(jwt.Claims); !ok && !token.Valid {
return false, err
}
return true, nil
}
package utils
import (
"crypto/tls"
"crypto/x509"
"os"
"strconv"
log "github.com/sirupsen/logrus"
)
var (
AdminName = os.Getenv("ADMIN_USERNAME")
AdminPassword = os.Getenv("ADMIN_PASSWORD")
DBUrl = os.Getenv("DB_SERVER")
DBUser = os.Getenv("DB_USER")
DBPassword = os.Getenv("DB_PASSWORD")
JWTExpiryDuration = getEnvAsInt("JWT_EXPIRY_MINS", 1440)
OAuthJWTExpDuration = getEnvAsInt("OAUTH_JWT_EXP_MINS", 5)
OAuthJwtSecret = os.Getenv("OAUTH_SECRET")
OAuthEnabled = getEnvAsBool("OAUTH_ENABLED", false)
OAuthCallBackURL = os.Getenv("OAUTH_CALLBACK_URL")
OAuthClientID = os.Getenv("OAUTH_CLIENT_ID")
OAuthClientSecret = os.Getenv("OAUTH_CLIENT_SECRET")
OAuthOIDCIssuer = os.Getenv("OIDC_ISSUER")
EnableInternalTls = getEnvAsBool("ENABLE_INTERNAL_TLS", false)
TlsCertPath = os.Getenv("TLS_CERT_PATH")
TlSKeyPath = os.Getenv("TLS_KEY_PATH")
CaCertPath = os.Getenv("CA_CERT_TLS_PATH")
RestPort = os.Getenv("REST_PORT")
GrpcPort = os.Getenv("GRPC_PORT")
DBName = "auth"
UserCollection = "users"
ProjectCollection = "project"
AuthConfigCollection = "auth-config"
RevokedTokenCollection = "revoked-token"
ApiTokenCollection = "api-token"
UsernameField = "username"
ExpiresAtField = "expires_at"
PasswordEncryptionCost = 8
DefaultLitmusGqlGrpcEndpoint = "localhost"
DefaultLitmusGqlGrpcPort = ":8000"
//DefaultLitmusGqlGrpcPortHttps = ":8001" // enable when in use
)
func getEnvAsInt(name string, defaultVal int) int {
valueStr := os.Getenv(name)
if value, err := strconv.Atoi(valueStr); err == nil {
return value
}
return defaultVal
}
func getEnvAsBool(name string, defaultVal bool) bool {
valueStr := os.Getenv(name)
if valueStr, err := strconv.ParseBool(valueStr); err == nil {
return valueStr
}
return defaultVal
}
func GetTlsConfig() *tls.Config {
// read ca's cert, verify to client's certificate
caPem, err := os.ReadFile(CaCertPath)
if err != nil {
log.Fatal(err)
}
// create cert pool and append ca's cert
certPool := x509.NewCertPool()
if !certPool.AppendCertsFromPEM(caPem) {
log.Fatal(err)
}
// read server cert & key
serverCert, err := tls.LoadX509KeyPair(TlsCertPath, TlSKeyPath)
if err != nil {
log.Fatal(err)
}
// configuring TLS config based on provided certificates & keys to
conf := &tls.Config{
Certificates: []tls.Certificate{serverCert},
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: certPool,
}
return conf
}
package utils
import (
"context"
"os"
grpc2 "github.com/litmuschaos/litmus/chaoscenter/authentication/api/presenter/protos"
"github.com/sirupsen/logrus"
"google.golang.org/grpc"
)
// GetProjectGRPCSvcClient returns an RPC client for Project service
func GetProjectGRPCSvcClient(conn *grpc.ClientConn) (grpc2.ProjectClient, *grpc.ClientConn) {
litmusGqlGrpcEndpoint := os.Getenv("LITMUS_GQL_GRPC_ENDPOINT")
litmusGqlGrpcPort := os.Getenv("LITMUS_GQL_GRPC_PORT")
if litmusGqlGrpcEndpoint == "" {
litmusGqlGrpcEndpoint = DefaultLitmusGqlGrpcEndpoint
}
if litmusGqlGrpcPort == "" {
litmusGqlGrpcPort = DefaultLitmusGqlGrpcPort
}
conn, err := grpc.Dial(litmusGqlGrpcEndpoint+litmusGqlGrpcPort, grpc.WithInsecure(), grpc.WithBlock())
if err != nil {
logrus.Fatalf("did not connect: %s", err)
}
return grpc2.NewProjectClient(conn), conn
}
// ProjectInitializer initializes a new project with default hub and image registry
func ProjectInitializer(context context.Context, client grpc2.ProjectClient, projectID string, role string) error {
_, err := client.InitializeProject(context,
&grpc2.ProjectInitializationRequest{
ProjectID: projectID,
Role: role,
})
return err
}
package utils
import (
"context"
"strings"
"time"
log "github.com/sirupsen/logrus"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
// MongoConnection creates a connection to the mongo
func MongoConnection() (*mongo.Client, error) {
ctx, _ := context.WithTimeout(context.Background(), 10*time.Second)
mongoCredentials := options.Credential{
Username: DBUser,
Password: DBPassword,
}
client, err := mongo.Connect(ctx, options.Client().ApplyURI(DBUrl).SetAuth(mongoCredentials))
if err != nil {
return nil, err
}
return client, nil
}
// CreateIndex creates a unique index for the given field in the collectionName
func CreateIndex(collectionName string, field string, db *mongo.Database) error {
mod := mongo.IndexModel{
Keys: bson.M{field: 1},
Options: options.Index().SetUnique(true),
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
collection := db.Collection(collectionName)
_, err := collection.Indexes().CreateOne(ctx, mod)
if err != nil {
log.Error(err)
return err
}
return nil
}
// CreateTTLIndex creates a TTL index for the given field in the collectionName
func CreateTTLIndex(collectionName string, db *mongo.Database) error {
// more info: https://www.mongodb.com/docs/manual/tutorial/expire-data/#expire-documents-at-a-specific-clock-time
mod := mongo.IndexModel{
Keys: bson.M{ExpiresAtField: 1},
Options: options.Index().SetExpireAfterSeconds(0),
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
collection := db.Collection(collectionName)
_, err := collection.Indexes().CreateOne(ctx, mod)
if err != nil {
log.Error(err)
return err
}
return nil
}
// CreateCollection creates a new mongo collection if it does not exist
func CreateCollection(collectionName string, db *mongo.Database) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err := db.CreateCollection(ctx, collectionName)
if err != nil {
if strings.Contains(err.Error(), "already exists") {
log.Info(collectionName + "'s collection already exists, continuing with the existing mongo collection")
return nil
} else {
return err
}
}
log.Info(collectionName + "'s mongo collection created")
return nil
}
package utils
import (
crypto "crypto/rand"
"encoding/base64"
"fmt"
"regexp"
"strings"
)
// SanitizeString trims the string input
func SanitizeString(input string) string {
return strings.TrimSpace(input)
}
/*
ValidateStrictPassword represents and checks for the following patterns:
- Input is at least 8 characters long and at most 16 characters long
- Input contains at least one special character of these @$!%*?_&#
- Input contains at least one digit
- Input contains at least one uppercase alphabet
- Input contains at least one lowercase alphabet
*/
func ValidateStrictPassword(input string) error {
if len(input) < 8 {
return fmt.Errorf("password length is less than 8 characters")
}
if len(input) > 16 {
return fmt.Errorf("password length is more than 16 characters")
}
digits := `[0-9]{1}`
lowerAlphabets := `[a-z]{1}`
capitalAlphabets := `[A-Z]{1}`
specialCharacters := `[@$!%*?_&#]{1}`
if b, err := regexp.MatchString(digits, input); !b || err != nil {
return fmt.Errorf("password does not contain digits")
}
if b, err := regexp.MatchString(lowerAlphabets, input); !b || err != nil {
return fmt.Errorf("password does not contain lowercase alphabets")
}
if b, err := regexp.MatchString(capitalAlphabets, input); !b || err != nil {
return fmt.Errorf("password does not contain uppercase alphabets")
}
if b, err := regexp.MatchString(specialCharacters, input); !b || err != nil {
return fmt.Errorf("password does not contain special characters")
}
return nil
}
// RandomString generates random strings, can be used to create ids
func RandomString(n int) (string, error) {
if n > 0 {
b := make([]byte, n)
_, err := crypto.Read(b)
if err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(b), nil
}
return "", fmt.Errorf("length should be greater than 0")
}
// Username must start with a letter or digit - ^[a-zA-Z0-9]
// Allow letters, digits, and the characters . _ - @ + (so an email address is a valid username,
// which is required for Dex SSO where the email is used as the username) - [a-zA-Z0-9._@+-]
// Ensure the length of the username is between 3 and 254 characters
// (1 character is already matched above, and 254 is the RFC 5321 maximum email length) - {2,253}$
func ValidateStrictUsername(username string) error {
if matched, _ := regexp.MatchString(`^[a-zA-Z0-9][a-zA-Z0-9._@+-]{2,253}$`, username); !matched {
return fmt.Errorf("username should be at least 3 characters long and at most 254 characters long, must start with a letter or digit, and can only contain letters, digits, and the characters . _ - @ +")
}
return nil
}
package authorization
import (
"context"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/litmuschaos/litmus/chaoscenter/graphql/server/pkg/metrics"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
)
type contextKey string
const (
AuthKey = contextKey("authorization")
UserClaim = contextKey("user-claims")
BearerSchema = "Bearer "
CookieName = "token"
)
// Middleware verifies jwt and checks if user has enough privilege to access route (no roles' info needed)
func Middleware(handler http.Handler, mongoClient *mongo.Client) gin.HandlerFunc {
return func(c *gin.Context) {
jwt := ""
authMethod := "bearer"
if c.Request.Header.Get("Authorization") != "" {
jwt = c.Request.Header.Get("Authorization")
}
if strings.HasPrefix(jwt, BearerSchema) {
jwt = jwt[len(BearerSchema):]
}
if IsRevokedToken(jwt, mongoClient) {
// Track authentication failure
metrics.AuthenticationFailuresTotal.WithLabelValues(authMethod).Inc()
c.Writer.WriteHeader(http.StatusUnauthorized)
c.Writer.Write([]byte("Error verifying JWT token: Token is revoked"))
return
}
ctx := context.WithValue(c.Request.Context(), AuthKey, jwt)
ctx1 := context.WithValue(ctx, "request-header", c.Request.Header)
c.Request = c.Request.WithContext(ctx1)
handler.ServeHTTP(c.Writer, c.Request)
}
}
// IsRevokedToken checks if the given JWT Token is revoked
func IsRevokedToken(tokenString string, mongoClient *mongo.Client) bool {
collection := mongoClient.Database("auth").Collection("revoked-token")
if err := collection.FindOne(context.Background(), bson.M{
"token": tokenString,
}).Err(); err != nil {
return false
}
return true
}
package authorization
import (
"context"
"errors"
"fmt"
"log"
"github.com/litmuschaos/litmus/chaoscenter/graphql/server/pkg/database/mongodb"
"github.com/litmuschaos/litmus/chaoscenter/graphql/server/pkg/database/mongodb/authConfig"
"github.com/golang-jwt/jwt/v4"
)
// UserValidateJWT validates the cluster jwt
func UserValidateJWT(token string, salt string) (jwt.MapClaims, error) {
tkn, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
if _, isValid := token.Method.(*jwt.SigningMethodHMAC); !isValid {
return nil, fmt.Errorf("invalid token %s", token.Header["alg"])
}
return []byte(salt), nil
})
if err != nil {
log.Print("USER JWT ERROR: ", err)
return nil, errors.New("invalid Token")
}
if !tkn.Valid {
return nil, errors.New("invalid Token")
}
claims, ok := tkn.Claims.(jwt.MapClaims)
if ok {
return claims, nil
}
return nil, errors.New("invalid Token")
}
// GetUsername returns the username from the jwt token
func GetUsername(token string) (string, error) {
salt, err := authConfig.NewAuthConfigOperator(mongodb.Operator).GetAuthConfig(context.Background())
if err != nil {
return "", err
}
tkn, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
return []byte(salt.Value), nil
})
if err != nil {
log.Print("USER JWT ERROR: ", err)
return "", errors.New("invalid Token")
}
claims, ok := tkn.Claims.(jwt.MapClaims)
if ok {
return claims["username"].(string), nil
}
return "", errors.New("invalid Token")
}
package authorization
import (
"context"
"errors"
"github.com/litmuschaos/litmus/chaoscenter/graphql/server/pkg/grpc"
"github.com/sirupsen/logrus"
grpc2 "google.golang.org/grpc"
)
// ValidateRole Validates the role of a user in a given project
func ValidateRole(ctx context.Context, projectID string,
requiredRoles []string, invitation string) error {
jwt := ctx.Value(AuthKey).(string)
var conn *grpc2.ClientConn
client, conn := grpc.GetAuthGRPCSvcClient(conn)
defer conn.Close()
err := grpc.ValidatorGRPCRequest(client, jwt, projectID,
requiredRoles,
invitation)
if err != nil {
logrus.Error(err)
return errors.New("permission_denied: " + err.Error())
}
return nil
}
package handler
import (
"archive/zip"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/mrz1836/go-sanitize"
"github.com/gin-gonic/gin"
"github.com/litmuschaos/litmus/chaoscenter/graphql/server/graph/model"
chaoshubops "github.com/litmuschaos/litmus/chaoscenter/graphql/server/pkg/chaoshub/ops"
"github.com/litmuschaos/litmus/chaoscenter/graphql/server/pkg/database/mongodb/chaos_hub"
"github.com/litmuschaos/litmus/chaoscenter/graphql/server/utils"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v2"
)
const DefaultPath = "/tmp/"
// GetChartsPath is used to construct path for given chart.
func GetChartsPath(chartsInput model.CloningInput, projectID string, isDefault bool) string {
var repoPath string
if isDefault {
repoPath = DefaultPath + "default/" + chartsInput.Name + "/faults/"
} else {
repoPath = DefaultPath + projectID + "/" + chartsInput.Name + "/faults/"
}
return repoPath
}
// GetChartsData is used to get details of charts like experiments.
func GetChartsData(chartsPath string) ([]*model.Chart, error) {
var allChartsDetails []ChaosChart
Charts, err := os.ReadDir(chartsPath)
if err != nil {
log.Error("file reading error", err)
return nil, err
}
for _, chart := range Charts {
if chart.Name() == "icons" {
continue
}
chartDetails, _ := ReadExperimentFile(chartsPath + chart.Name() + "/" + chart.Name() + ".chartserviceversion.yaml")
allChartsDetails = append(allChartsDetails, chartDetails)
}
e, err := json.Marshal(allChartsDetails)
if err != nil {
return nil, err
}
var unmarshalledData []*model.Chart
err = json.Unmarshal(e, &unmarshalledData)
if err != nil {
return nil, err
}
return unmarshalledData, nil
}
// GetExperimentData is used for getting details of selected Experiment path
func GetExperimentData(experimentFilePath string) (*model.Chart, error) {
data, err := ReadExperimentFile(experimentFilePath)
if err != nil {
return nil, err
}
e, err := json.Marshal(data)
if err != nil {
return nil, err
}
var chartData *model.Chart
if err = json.Unmarshal(e, &chartData); err != nil {
return nil, err
}
return chartData, nil
}
// ReadExperimentFile is used for reading experiment file from given path
func ReadExperimentFile(path string) (ChaosChart, error) {
var experiment ChaosChart
experimentFile, err := os.ReadFile(path)
if err != nil {
return experiment, fmt.Errorf("file path of the, err: %+v", err)
}
if err = yaml.Unmarshal(experimentFile, &experiment); err != nil {
return experiment, err
}
return experiment, nil
}
// ReadExperimentYAMLFile is used for reading experiment/engine file from given path
func ReadExperimentYAMLFile(path string) (string, error) {
var s string
YAMLData, err := os.ReadFile(path)
if err != nil {
return s, fmt.Errorf("file path of the, err: %+v", err)
}
s = string(YAMLData)
return s, nil
}
// ListPredefinedWorkflowDetails reads the workflow directory for all the predefined experiments
// and returns the csv, workflow manifest and workflow name
func ListPredefinedWorkflowDetails(name string, projectID string) ([]*model.PredefinedExperimentList, error) {
experimentsPath := DefaultPath + projectID + "/" + name + "/workflows"
var predefinedWorkflows []*model.PredefinedExperimentList
files, err := os.ReadDir(experimentsPath)
if err != nil {
return nil, err
}
for _, file := range files {
csvManifest := ""
workflowManifest := ""
isExist, err := IsFileExisting(experimentsPath + "/" + file.Name() + "/" + file.Name() + ".chartserviceversion.yaml")
if err != nil {
return nil, err
}
if isExist {
csvManifest, err = ReadExperimentYAMLFile(experimentsPath + "/" + file.Name() + "/" + file.Name() + ".chartserviceversion.yaml")
if err != nil {
csvManifest = "na"
}
workflowManifest, err = ReadExperimentYAMLFile(experimentsPath + "/" + file.Name() + "/" + "workflow.yaml")
if err != nil {
workflowManifest = "na"
}
preDefinedWorkflow := &model.PredefinedExperimentList{
ExperimentName: file.Name(),
ExperimentManifest: workflowManifest,
ExperimentCSV: csvManifest,
}
predefinedWorkflows = append(predefinedWorkflows, preDefinedWorkflow)
}
}
return predefinedWorkflows, nil
}
func IsFileExisting(path string) (bool, error) {
_, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
}
return true, nil
}
// DownloadRemoteHub is used to download a remote hub from the url provided by the user
func DownloadRemoteHub(hubDetails model.CreateRemoteChaosHub, projectID string) error {
dirPath := DefaultPath + projectID
err := os.MkdirAll(dirPath, 0755)
if err != nil {
return err
}
//create the destination directory where the hub will be downloaded
hubPath := dirPath + "/" + hubDetails.Name + ".zip"
destDir, err := os.Create(hubPath)
if err != nil {
log.Error(err)
return err
}
defer destDir.Close()
//download the zip file from the provided url
download, err := http.Get(sanitize.URL(hubDetails.RepoURL))
if err != nil {
log.Error(err)
return err
}
defer download.Body.Close()
if download.StatusCode != http.StatusOK {
return fmt.Errorf("err: %v", download.Status)
}
//validate the content length (in bytes)
maxSize, err := strconv.Atoi(utils.Config.RemoteHubMaxSize)
if err != nil {
return err
}
contentLength := download.Header.Get("content-length")
length, err := strconv.Atoi(contentLength)
if length > maxSize {
_ = os.Remove(hubPath)
return fmt.Errorf("err: File size exceeded the threshold %d", length)
}
//validate the content-type
contentType := download.Header.Get("content-type")
if contentType != "application/zip" {
_ = os.Remove(hubPath)
return fmt.Errorf("err: Invalid file type %s", contentType)
}
//copy the downloaded content to the created zip file
_, err = io.Copy(destDir, download.Body)
if err != nil {
log.Error(err)
return err
}
//unzip the ChaosHub to the default hub directory
err = UnzipRemoteHub(hubPath, projectID)
if err != nil {
return err
}
//remove the redundant zip file
err = os.Remove(hubPath)
if err != nil {
return err
}
return nil
}
// UnzipRemoteHub is used to unzip the zip file
func UnzipRemoteHub(zipPath string, projectID string) error {
extractPath := DefaultPath + projectID
zipReader, err := zip.OpenReader(zipPath)
if err != nil {
log.Error(err)
return err
}
defer func(zipReader *zip.ReadCloser) {
err := zipReader.Close()
if err != nil {
log.Error(err)
}
}(zipReader)
for _, file := range zipReader.File {
err := CopyZipItems(file, extractPath, file.Name)
if err != nil {
return err
}
}
return nil
}
// CopyZipItems is used to copy the content from the extracted zip file to
// the ChaosHub directory
func CopyZipItems(file *zip.File, extractPath string, chartsPath string) error {
path := filepath.Join(extractPath, chartsPath)
if !strings.HasPrefix(path, filepath.Clean(extractPath)+string(os.PathSeparator)) {
return fmt.Errorf("illegal file path: %s", path)
}
err := os.MkdirAll(filepath.Dir(path), os.ModeDir|os.ModePerm)
if err != nil {
log.Error(err)
}
fileReader, err := file.Open()
if err != nil {
log.Error(err)
}
if !file.FileInfo().IsDir() {
fileCopy, err := os.Create(path)
if err != nil {
log.Error(err)
}
_, err = io.Copy(fileCopy, fileReader)
if err != nil {
log.Error(err)
}
fileCopy.Close()
}
fileReader.Close()
return nil
}
// SyncRemoteRepo is used to sync the remote ChaosHub
func SyncRemoteRepo(hubData model.CloningInput, projectID string) error {
hubPath := DefaultPath + projectID + "/" + hubData.Name
err := os.RemoveAll(hubPath)
if err != nil {
return err
}
updateHub := model.CreateRemoteChaosHub{
Name: hubData.Name,
RepoURL: hubData.RepoURL,
}
log.Info("downloading remote hub")
err = DownloadRemoteHub(updateHub, projectID)
if err != nil {
return err
}
log.Info("remote hub ", hubData.Name, "downloaded ")
return nil
}
// ValidateLocalRepository validates the repository directory and checks it by plain opening it.
func ValidateLocalRepository(hub chaos_hub.ChaosHub) (bool, error) {
var repoPath string
if hub.IsDefault {
repoPath = DefaultPath + "default/" + hub.Name
} else {
repoPath = DefaultPath + hub.ProjectID + "/" + hub.Name
}
err := chaoshubops.GitPlainOpen(repoPath)
if err != nil {
return false, err
}
return true, nil
}
// ChaosHubIconHandler is used for fetching ChaosHub icons
func ChaosHubIconHandler() gin.HandlerFunc {
return func(c *gin.Context) {
var (
img *os.File
err error
responseStatusCode int
)
if strings.ToLower(c.Param("chartName")) == "predefined" {
img, err = os.Open(utils.Config.CustomChaosHubPath + c.Param("projectId") + "/" + c.Param("hubName") + "/experiments/icons/" + c.Param("iconName"))
responseStatusCode = http.StatusOK
if err != nil {
responseStatusCode = http.StatusInternalServerError
log.WithError(err).Error("icon cannot be fetched")
fmt.Fprint(c.Writer, "icon cannot be fetched, err : "+err.Error())
}
} else {
img, err = os.Open(utils.Config.CustomChaosHubPath + c.Param("projectId") + "/" + c.Param("hubName") + "/faults/" + c.Param("chartName") + "/icons/" + c.Param("iconName"))
responseStatusCode = http.StatusOK
if err != nil {
responseStatusCode = http.StatusInternalServerError
log.WithError(err).Error("icon cannot be fetched")
fmt.Fprint(c.Writer, "icon cannot be fetched, err : "+err.Error())
}
}
defer img.Close()
c.Writer.Header().Set("Content-Type", "image/png")
c.Writer.WriteHeader(responseStatusCode)
io.Copy(c.Writer, img)
}
}
func DefaultChaosHubIconHandler() gin.HandlerFunc {
return func(c *gin.Context) {
var (
img *os.File
err error
responseStatusCode int
)
if strings.ToLower(c.Param("chartName")) == "predefined" {
img, err = os.Open(utils.Config.DefaultChaosHubPath + c.Param("hubName") + "/experiments/icons/" + c.Param("iconName"))
responseStatusCode = http.StatusOK
if err != nil {
responseStatusCode = http.StatusInternalServerError
log.WithError(err).Error("icon cannot be fetched")
fmt.Fprint(c.Writer, "icon cannot be fetched, err : "+err.Error())
}
} else {
img, err = os.Open(utils.Config.DefaultChaosHubPath + c.Param("hubName") + "/faults/" + c.Param("chartName") + "/icons/" + c.Param("iconName"))
responseStatusCode = http.StatusOK
if err != nil {
responseStatusCode = http.StatusInternalServerError
log.WithError(err).Error("icon cannot be fetched")
fmt.Fprint(c.Writer, "icon cannot be fetched, err : "+err.Error())
}
}
defer func(img *os.File) {
err := img.Close()
if err != nil {
log.WithError(err).Error("error while closing the file")
}
}(img)
c.Writer.Header().Set("Content-Type", "image/png")
c.Writer.WriteHeader(responseStatusCode)
io.Copy(c.Writer, img)
}
}
package chaoshubops
import (
"fmt"
"os"
"strings"
"github.com/litmuschaos/litmus/chaoscenter/graphql/server/graph/model"
"github.com/litmuschaos/litmus/chaoscenter/graphql/server/utils"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/protocol/packp/capability"
"github.com/go-git/go-git/v5/plumbing/transport"
"github.com/go-git/go-git/v5/plumbing/transport/http"
"github.com/go-git/go-git/v5/plumbing/transport/ssh"
ssh2 "golang.org/x/crypto/ssh"
log "github.com/sirupsen/logrus"
)
const DefaultPath = "/tmp/"
// ChaosHubConfig is the config used for all git operations
type ChaosHubConfig struct {
ProjectID string
RepositoryURL string
RemoteName string
LocalCommit string
RemoteCommit string
HubName string
Branch string
IsPrivate bool
IsDefault bool
UserName *string
Password *string
AuthType model.AuthType
Token *string
SSHPrivateKey *string
}
// GetClonePath is used to construct path for Repository.
func GetClonePath(c ChaosHubConfig) string {
var repoPath string
if c.IsDefault {
repoPath = "/tmp/default/" + c.HubName
} else {
repoPath = DefaultPath + c.ProjectID + "/" + c.HubName
}
return repoPath
}
// GitConfigConstruct is used for constructing the gitconfig
func GitConfigConstruct(repoData model.CloningInput, projectID string) ChaosHubConfig {
gitConfig := ChaosHubConfig{
ProjectID: projectID,
HubName: repoData.Name,
RepositoryURL: repoData.RepoURL,
RemoteName: "origin",
Branch: repoData.RepoBranch,
IsPrivate: repoData.IsPrivate,
UserName: repoData.UserName,
Password: repoData.Password,
AuthType: repoData.AuthType,
Token: repoData.Token,
SSHPrivateKey: repoData.SSHPrivateKey,
IsDefault: repoData.IsDefault,
}
return gitConfig
}
// GitClone Trigger is responsible for setting off the go routine for git-op
func GitClone(repoData model.CloningInput, projectID string) error {
gitConfig := GitConfigConstruct(repoData, projectID)
if repoData.IsPrivate {
_, err := gitConfig.getPrivateChaosChartRepo()
if err != nil {
return fmt.Errorf("error in cloning private repo: %v", err)
}
} else {
_, err := gitConfig.getChaosChartRepo()
if err != nil {
return fmt.Errorf("error in cloning public repo: %v", err)
}
}
// Successfully Cloned
return nil
}
// getChaosChartVersion is responsible for plain cloning the repository
func (c ChaosHubConfig) getChaosChartRepo() (string, error) {
ClonePath := GetClonePath(c)
os.RemoveAll(ClonePath)
_, err := git.PlainClone(ClonePath, false, &git.CloneOptions{
URL: c.RepositoryURL,
Progress: nil,
ReferenceName: plumbing.NewBranchReferenceName(c.Branch),
SingleBranch: true,
})
if err != nil {
_, err = git.PlainClone(ClonePath, false, &git.CloneOptions{
URL: c.RepositoryURL,
Progress: nil,
ReferenceName: plumbing.NewTagReferenceName(c.Branch),
SingleBranch: true,
})
return c.Branch, err
}
return c.Branch, err
}
// getPrivateChaosChartVersion is responsible for plain cloning the private repository
func (c ChaosHubConfig) getPrivateChaosChartRepo() (string, error) {
ClonePath := GetClonePath(c)
os.RemoveAll(ClonePath)
auth, err := c.generateAuthMethod()
if err != nil {
return "", err
}
_, err = git.PlainClone(ClonePath, false, &git.CloneOptions{
Auth: auth,
URL: c.RepositoryURL,
Progress: nil,
SingleBranch: true,
ReferenceName: plumbing.NewBranchReferenceName(c.Branch),
})
if err != nil {
_, err = git.PlainClone(ClonePath, false, &git.CloneOptions{
Auth: auth,
URL: c.RepositoryURL,
Progress: nil,
SingleBranch: true,
ReferenceName: plumbing.NewTagReferenceName(c.Branch),
})
return c.Branch, err
}
return c.Branch, err
}
// GitSyncHandlerForProjects ...
func GitSyncHandlerForProjects(repoData model.CloningInput, projectID string) error {
gitConfig := GitConfigConstruct(repoData, projectID)
if err := gitConfig.chaosChartSyncHandler(); err != nil {
log.Error(err)
return err
}
// Repository syncing completed
return nil
}
// GitSyncDefaultHub ...
func GitSyncDefaultHub(repoData model.CloningInput) error {
gitConfig := GitConfigConstruct(repoData, "")
if err := gitConfig.chaosChartSyncHandler(); err != nil {
log.Error(err)
return err
}
return nil
}
// chaosChartSyncHandler is responsible for all the handler functions
func (c ChaosHubConfig) chaosChartSyncHandler() error {
repositoryExists, err := c.isRepositoryExists()
if err != nil {
return fmt.Errorf("Error while checking repo exists, err: %s", err)
}
log.WithFields(log.Fields{"repositoryExists": repositoryExists}).Info("executed isRepositoryExists()... ")
if !repositoryExists {
return GitClone(model.CloningInput{
Name: c.HubName,
RepoURL: c.RepositoryURL,
RepoBranch: c.Branch,
IsPrivate: c.IsPrivate,
AuthType: c.AuthType,
Token: c.Token,
UserName: c.UserName,
Password: c.Password,
SSHPrivateKey: c.SSHPrivateKey,
IsDefault: c.IsDefault,
}, c.ProjectID)
}
return c.GitPull()
}
// isRepositoryExists checks for the existence of this past existence of this repository
func (c ChaosHubConfig) isRepositoryExists() (bool, error) {
RepoPath := GetClonePath(c)
_, err := os.Stat(RepoPath)
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
return true, nil
}
func (c ChaosHubConfig) setterRepositoryWorktreeReference() (*git.Repository, *git.Worktree, *plumbing.Reference, error) {
RepoPath := GetClonePath(c)
repository, err := git.PlainOpen(RepoPath)
if err != nil {
return nil, nil, nil, fmt.Errorf("error in executing PlainOpen: %s", err)
}
workTree, err := repository.Worktree()
if err != nil {
return nil, nil, nil, fmt.Errorf("error in executing Worktree: %s", err)
}
plumbingRef, err := repository.Head()
if err != nil {
return nil, nil, nil, fmt.Errorf("error in executing Head: %s", err)
}
return repository, workTree, plumbingRef, nil
}
func GitPlainOpen(repoPath string) error {
_, err := git.PlainOpen(repoPath)
if err != nil {
return err
}
return nil
}
// GitPull updates the repository in provided Path
func (c ChaosHubConfig) GitPull() error {
_, workTree, plumbingRef, err := c.setterRepositoryWorktreeReference()
if err != nil {
return err
}
var referenceName plumbing.ReferenceName
referenceName = plumbing.NewBranchReferenceName(c.Branch)
if !c.IsPrivate {
err = workTree.Pull(&git.PullOptions{RemoteName: c.RemoteName, ReferenceName: referenceName})
if err == git.NoErrAlreadyUpToDate {
log.Info("already up-to-date")
return nil
} else if err != nil {
return err
}
c.LocalCommit = strings.Split(plumbingRef.String(), " ")[0]
return nil
}
err = c.gitPullPrivateRepo()
if err == git.NoErrAlreadyUpToDate {
log.Info("already up-to-date")
return nil
} else if err != nil {
return err
}
return nil
}
// gitPullPrivateRepo updates the repository of private hubs
func (c ChaosHubConfig) gitPullPrivateRepo() error {
_, workTree, _, err := c.setterRepositoryWorktreeReference()
if err != nil {
return err
}
var referenceName plumbing.ReferenceName
referenceName = plumbing.NewBranchReferenceName(c.Branch)
auth, err := c.generateAuthMethod()
if err != nil {
return nil
}
err = workTree.Pull(&git.PullOptions{RemoteName: c.RemoteName, ReferenceName: referenceName, Auth: auth})
if err != nil {
return err
}
return nil
}
// generateAuthMethod creates AuthMethod for private repos
func (c ChaosHubConfig) generateAuthMethod() (transport.AuthMethod, error) {
transport.UnsupportedCapabilities = []capability.Capability{
capability.ThinPack,
}
var auth transport.AuthMethod
if c.AuthType == model.AuthTypeToken {
auth = &http.BasicAuth{
Username: utils.Config.GitUsername, // must be a non-empty string or 'x-token-auth' for Bitbucket
Password: *c.Token,
}
} else if c.AuthType == model.AuthTypeBasic {
auth = &http.BasicAuth{
Username: *c.UserName,
Password: *c.Password,
}
} else if c.AuthType == model.AuthTypeSSH {
publicKey, err := ssh.NewPublicKeys("git", []byte(*c.SSHPrivateKey), "")
if err != nil {
return nil, err
}
auth = publicKey
auth.(*ssh.PublicKeys).HostKeyCallback = ssh2.InsecureIgnoreHostKey()
}
return auth, nil
}
package chaoshubops
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh"
)
// GenerateKeys ...
func GenerateKeys() (string, string, error) {
bitSize := 4096
privateKey, err := generatePrivateKey(bitSize)
if err != nil {
return "", "", err
}
publicKeyBytes, err := generatePublicKey(&privateKey.PublicKey)
if err != nil {
return "", "", err
}
privateKeyBytes := encodePrivateKeyToPEM(privateKey)
return string(publicKeyBytes), string(privateKeyBytes), nil
}
// generatePrivateKey creates a RSA Private Key of specified byte size
func generatePrivateKey(bitSize int) (*rsa.PrivateKey, error) {
// Private Key generation
privateKey, err := rsa.GenerateKey(rand.Reader, bitSize)
if err != nil {
return nil, err
}
// Validate Private Key
err = privateKey.Validate()
if err != nil {
return nil, err
}
log.Info("private Key generated")
return privateKey, nil
}
// encodePrivateKeyToPEM encodes Private Key from RSA to PEM format
func encodePrivateKeyToPEM(privateKey *rsa.PrivateKey) []byte {
// Get ASN.1 DER format
privDER := x509.MarshalPKCS1PrivateKey(privateKey)
// pem.Block
privBlock := pem.Block{
Type: "RSA PRIVATE KEY",
Headers: nil,
Bytes: privDER,
}
// Private key in PEM format
privatePEM := pem.EncodeToMemory(&privBlock)
return privatePEM
}
// generatePublicKey take a rsa.PublicKey and return bytes suitable for writing to .pub file
// returns in the format "ssh-rsa ..."
func generatePublicKey(privatekey *rsa.PublicKey) ([]byte, error) {
publicRsaKey, err := ssh.NewPublicKey(privatekey)
if err != nil {
return nil, err
}
pubKeyBytes := ssh.MarshalAuthorizedKey(publicRsaKey)
log.Info("public key generated")
return pubKeyBytes, nil
}