├── .env ├── .github └── workflows │ └── version-release.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── assets └── kriten.png ├── config ├── config.go └── db.go ├── controllers ├── audit.go ├── auth.go ├── cronjobs.go ├── groups.go ├── job.go ├── role_bindings.go ├── roles.go ├── runner.go ├── task.go ├── tokens.go ├── users.go └── webhooks.go ├── docs ├── docs.go ├── swagger.json └── swagger.yaml ├── go.mod ├── go.sum ├── helpers ├── auth.go ├── elastic.go ├── httpError.go ├── k8s.go └── ldap.go ├── main.go ├── middlewares └── auth.go ├── models ├── audit.go ├── credentials.go ├── cronjob.go ├── group.go ├── job.go ├── role.go ├── role_binding.go ├── runner.go ├── task.go ├── token.go ├── user.go └── webhook.go ├── services ├── audit_svc.go ├── auth_svc.go ├── cronjobs_svc.go ├── groups_svc.go ├── job_svc.go ├── role_bindings_svc.go ├── roles_svc.go ├── runner_svc.go ├── task_svc.go ├── tokens_svc.go ├── users_svc.go └── webhook_svc.go └── spec.json /.env: -------------------------------------------------------------------------------- 1 | ENV = "" 2 | 3 | # Name of the secret containing root password 4 | ROOT_SECRET = "kriten-root" 5 | 6 | # Kubernetes settings 7 | NAMESPACE = "kriten" 8 | JOBS_TTL = 3600 9 | 10 | # LDAP Active Directory variables 11 | LDAP_BIND_USER = "" 12 | LDAP_BIND_PASS = "" 13 | LDAP_FQDN = "" 14 | LDAP_PORT = 389 # 636 for TLS 15 | LDAP_BASE_DN = "" 16 | 17 | # JWT and API configs 18 | JWT_KEY = "" 19 | JWT_EXPIRY_SECONDS = 3600 # value in seconds, 3600 seconds = 1 hour 20 | API_SECRET_KEY = "" 21 | 22 | # Postgres connection config 23 | DB_NAME = "" 24 | DB_HOST = "" 25 | DB_USER = "" 26 | DB_PASSWORD = "" 27 | DB_PORT = 5432 28 | DB_SSL = "disable" 29 | 30 | # Elastic Search 31 | ES_CLOUD_ID = "" 32 | ES_API_KEY = "" 33 | ES_INDEX = "" 34 | -------------------------------------------------------------------------------- /.github/workflows/version-release.yml: -------------------------------------------------------------------------------- 1 | name: Version Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | jobs: 9 | container-images: 10 | name: Build and push container images 11 | runs-on: ubuntu-latest 12 | env: 13 | DOCKER_REGISTRY: hub.docker.com 14 | DOCKER_REPOSITORY: kriten 15 | DOCKER_PLATFORM: linux/amd64,linux/arm64 16 | steps: 17 | - name: Check out the repo 18 | uses: actions/checkout@v3 19 | 20 | - name: Generates Swag docs 21 | uses: yegorrybchenko/go-swag-action@v0.1 22 | with: 23 | command: init --parseDependency --parseInternal 24 | swagWersion: 1.8.12 25 | 26 | - name: Set up QEMU 27 | uses: docker/setup-qemu-action@v3 28 | 29 | - name: Set up Docker Buildx 30 | uses: docker/setup-buildx-action@v3 31 | 32 | - name: Log in to Docker Hub 33 | uses: docker/login-action@v2 34 | with: 35 | username: ${{ secrets.DOCKER_USERNAME }} 36 | password: ${{ secrets.DOCKER_PASSWORD }} 37 | 38 | - name: Extract metadata (tags, labels) for Docker 39 | id: meta 40 | uses: docker/metadata-action@v3 41 | with: 42 | images: ${{ secrets.DOCKER_USERNAME }}/${{ env.DOCKER_REPOSITORY }} 43 | 44 | - name: Build and push Docker image 45 | uses: docker/build-push-action@v6 46 | with: 47 | context: . 48 | push: true 49 | platforms: ${{ env.DOCKER_PLATFORM }} 50 | tags: ${{ steps.meta.outputs.tags }} 51 | labels: ${{ steps.meta.outputs.labels }} 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | brainiac-core 8 | 9 | # Test binary, built with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Dependency directories (remove the comment below to include it) 16 | # vendor/ 17 | 18 | # Local inventory file 19 | .env 20 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.23 as builder 2 | 3 | WORKDIR /workspace 4 | # Copy the Go Modules manifests 5 | COPY go.mod go.mod 6 | COPY go.sum go.sum 7 | # cache deps before building and copying source so that we don't need to re-download as much 8 | # and so that source changes don't invalidate our downloaded layer 9 | RUN go mod download 10 | 11 | # Copy the go source 12 | COPY . . 13 | 14 | RUN GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD) 15 | # Build 16 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o kriten -ldflags "-X main.GitBranch=$GIT_BRANCH" 17 | 18 | # Use distroless as minimal base image to package the kriten binary 19 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 20 | FROM gcr.io/distroless/static:nonroot 21 | WORKDIR / 22 | COPY --from=builder /workspace/kriten . 23 | COPY --from=builder /workspace/.env . 24 | COPY --from=builder /workspace/spec.json . 25 | USER 65532:65532 26 | 27 | EXPOSE 8080 28 | 29 | ENTRYPOINT ["/kriten"] 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Kriten 3 |
4 | Kriten 5 |

6 | 7 |

Visit kriten.io for the full documentation, examples and guides.

8 | 9 | 10 |
11 | 12 | [![Version Release](https://github.com/Kriten-io/kriten/actions/workflows/version-release.yml/badge.svg)](https://github.com/Kriten-io/kriten/actions/workflows/version-release.yml) 13 | 14 |
15 | 16 |

17 | Quickstart • 18 | Contributing • 19 | Credits • 20 | License 21 |

22 | 23 | * Code execution platform 24 | * Written for and runs on [Kubernetes](https://kubernetes.io/) 25 | * Automated REST API exposure 26 | - Your custom code will be made available through dynamically created endpoints 27 | * Granular RBAC control 28 | * Local and Active Directory user authentication 29 | 30 | ## Quickstart 31 | 32 | Kriten is avaible to be installed with [Helm](https://helm.sh/). From your command line: 33 | 34 | ```bash 35 | # Add kriten Repo to Helm and update 36 | $ helm repo add kriten https://kriten-io.github.io/kriten-charts/ 37 | $ helm repo update 38 | 39 | # Create a namespace in your cluster 40 | $ kubectl create ns kriten 41 | 42 | # Install Kriten 43 | $ helm install kriten kriten/kriten -n kriten 44 | ``` 45 | 46 | > **Note** 47 | > You may want to modify the default values before install, more info on the installation can be found [here](https://kriten.io/#installation). 48 | 49 | 50 | ## Contributing 51 | 52 | Kriten welcomes users feedback and ideas, feel free to raise an issue on GitHub if you need any help. 53 | 54 | ## Credits 55 | 56 | This software uses the following open source packages: 57 | 58 | - [Gin](https://gin-gonic.com/) 59 | - [K8s client-go](https://github.com/kubernetes/client-go/) 60 | - [Swaggo](https://github.com/swaggo/swag) 61 | - [GORM](https://gorm.io/) 62 | 63 | ## Contact us 64 | 65 | Email to . 66 | 67 | Find us on [Slack](https://netdev-community.slack.com/archives/C06PJKB2HUJ). 68 | 69 | ## License 70 | 71 | GNU General Public License v3.0, see [LICENSE](https://github.com/kriten-io/kriten/blob/main/LICENSE). 72 | 73 | -------------------------------------------------------------------------------- /assets/kriten.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kriten-io/kriten/26a822d44334daf37cc6c4c45fc5c393b5f9ac12/assets/kriten.png -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | "strconv" 6 | 7 | "k8s.io/client-go/kubernetes" 8 | ) 9 | 10 | const ( 11 | JobsTTLDefault = 3600 12 | JWTExpirySecondsDefault = 3600 13 | ) 14 | 15 | type LDAPConfig struct { 16 | BindUser string 17 | BindPass string 18 | FQDN string 19 | BaseDN string 20 | Port int 21 | } 22 | 23 | type KubeConfig struct { 24 | Clientset *kubernetes.Clientset 25 | Namespace string 26 | JobsTTL int 27 | } 28 | 29 | type JWTConfig struct { 30 | Key []byte 31 | ExpirySeconds int 32 | } 33 | 34 | type DBConfig struct { 35 | Name string 36 | Host string 37 | User string 38 | Password string 39 | SSL string 40 | Port int 41 | } 42 | 43 | type Config struct { 44 | Environment string 45 | RootSecret string 46 | APISecret string 47 | LDAP LDAPConfig 48 | Kube KubeConfig 49 | JWT JWTConfig 50 | DB DBConfig 51 | DebugMode bool 52 | } 53 | 54 | // NewConfig returns a new Config struct. 55 | func NewConfig(gitBranch string) Config { 56 | return Config{ 57 | Environment: getEnv("ENV", "development"), 58 | RootSecret: getEnv("ROOT_SECRET", "kriten-root"), 59 | APISecret: getEnv("API_SECRET_KEY", "api-secret"), 60 | DebugMode: getEnvAsBool("DEBUG_MODE", true), 61 | LDAP: LDAPConfig{ 62 | BindUser: getEnv("LDAP_BIND_USER", ""), 63 | BindPass: getEnv("LDAP_BIND_PASS", ""), 64 | FQDN: getEnv("LDAP_FQDN", ""), 65 | Port: getEnvAsInt("LDAP_PORT", -1), 66 | BaseDN: getEnv("LDAP_BASE_DN", ""), 67 | }, 68 | Kube: KubeConfig{ 69 | Clientset: nil, 70 | Namespace: getEnv("NAMESPACE", "kriten"), 71 | JobsTTL: getEnvAsInt("JOBS_TTL", JobsTTLDefault), // Default 1 hour 72 | }, 73 | JWT: JWTConfig{ 74 | Key: []byte(getEnv("JWT_KEY", "")), 75 | ExpirySeconds: getEnvAsInt("JWT_EXPIRY_SECONDS", JWTExpirySecondsDefault), // Default 1 hour expiry 76 | }, 77 | DB: DBConfig{ 78 | Name: getEnv("DB_NAME", ""), 79 | Host: getEnv("DB_HOST", ""), 80 | User: getEnv("DB_USER", ""), 81 | Password: getEnv("DB_PASSWORD", ""), 82 | Port: getEnvAsInt("DB_PORT", -1), 83 | SSL: getEnv("DB_SSL", "disabled"), 84 | }, 85 | // ElasticSearch: ESConfig{ 86 | // CloudID: getEnv("ES_CLOUD_ID", ""), 87 | // APIKey: getEnv("ES_API_KEY", ""), 88 | // Index: getEnv("ES_INDEX", ""), 89 | // }, 90 | } 91 | } 92 | 93 | // Simple helper function to read an environment or return a default value. 94 | func getEnv(key string, defaultVal string) string { 95 | if value, exists := os.LookupEnv(key); exists && value != "" { 96 | return value 97 | } 98 | 99 | return defaultVal 100 | } 101 | 102 | // Simple helper function to read an environment variable into integer or return a default value. 103 | func getEnvAsInt(name string, defaultVal int) int { 104 | valueStr := getEnv(name, "") 105 | if value, err := strconv.Atoi(valueStr); err == nil { 106 | return value 107 | } 108 | 109 | return defaultVal 110 | } 111 | 112 | // Helper to read an environment variable into a bool or return default value. 113 | func getEnvAsBool(name string, defaultVal bool) bool { 114 | valStr := getEnv(name, "") 115 | if val, err := strconv.ParseBool(valStr); err == nil { 116 | return val 117 | } 118 | 119 | return defaultVal 120 | } 121 | -------------------------------------------------------------------------------- /config/db.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/kriten-io/kriten/models" 7 | "github.com/lib/pq" 8 | 9 | "gorm.io/gorm" 10 | ) 11 | 12 | func InitDB(db *gorm.DB) { 13 | err := db.AutoMigrate( 14 | &models.AuditLog{}, 15 | &models.Group{}, 16 | &models.Role{}, 17 | &models.RoleBinding{}, 18 | &models.User{}, 19 | &models.ApiToken{}, 20 | &models.Webhook{}, 21 | ) 22 | if err != nil { 23 | log.Println("Error during Postgres AutoMigrate") 24 | log.Println(err) 25 | } 26 | 27 | var root = models.User{Username: "root", Provider: "local", Builtin: true, Groups: pq.StringArray{}} 28 | db.FirstOrCreate(&root) 29 | 30 | if err := db.Where(&root). 31 | Assign(&root). 32 | FirstOrCreate(&models.User{}).Error; err != nil { 33 | return 34 | } 35 | 36 | var adminRole = models.Role{ 37 | Name: "Admin", Resource: "*", Resource_IDs: pq.StringArray{"*"}, Access: "write", Builtin: true, 38 | } 39 | db.FirstOrCreate(&adminRole) 40 | 41 | var adminGroup = models.Group{ 42 | Name: "Admin", Provider: "local", Users: pq.StringArray{root.ID.String()}, Builtin: true, 43 | } 44 | db.FirstOrCreate(&adminGroup) 45 | 46 | root.Groups = pq.StringArray{adminGroup.ID.String()} 47 | db.Updates(&root) 48 | 49 | var adminRoleBindings = models.RoleBinding{ 50 | Name: "RootAdminAccess", 51 | RoleID: adminRole.ID, 52 | RoleName: "Admin", 53 | SubjectID: adminGroup.ID, 54 | SubjectName: "root", 55 | SubjectKind: "root", 56 | SubjectProvider: "local", 57 | Builtin: true, 58 | } 59 | db.Create(&adminRoleBindings) 60 | 61 | var builtinRoles = []models.Role{ 62 | {Name: "WriteAllRunners", Resource: "runners", Resource_IDs: pq.StringArray{"*"}, Access: "write", Builtin: true}, 63 | {Name: "WriteAllTasks", Resource: "tasks", Resource_IDs: pq.StringArray{"*"}, Access: "write", Builtin: true}, 64 | {Name: "WriteAllJobs", Resource: "jobs", Resource_IDs: pq.StringArray{"*"}, Access: "write", Builtin: true}, 65 | {Name: "WriteAllUsers", Resource: "users", Resource_IDs: pq.StringArray{"*"}, Access: "write", Builtin: true}, 66 | {Name: "WriteAllRoles", Resource: "roles", Resource_IDs: pq.StringArray{"*"}, Access: "write", Builtin: true}, 67 | {Name: "WriteAllRoleBindings", Resource: "role_bindings", Resource_IDs: pq.StringArray{"*"}, Access: "write", Builtin: true}, 68 | } 69 | db.Create(&builtinRoles) 70 | 71 | // rules to preveng builtin deletion or update 72 | db.Exec("CREATE RULE builtin_del_users AS ON DELETE TO users WHERE builtin DO INSTEAD nothing;") 73 | db.Exec("CREATE RULE builtin_upd_users AS ON UPDATE TO users WHERE old.builtin DO INSTEAD nothing;") 74 | db.Exec("CREATE RULE builtin_del_groups AS ON DELETE TO groups WHERE builtin DO INSTEAD nothing;") 75 | db.Exec("CREATE RULE builtin_upd_groups AS ON UPDATE TO groups WHERE old.builtin DO INSTEAD nothing;") 76 | db.Exec("CREATE RULE builtin_del_roles AS ON DELETE TO roles WHERE builtin DO INSTEAD nothing;") 77 | db.Exec("CREATE RULE builtin_upd_roles AS ON UPDATE TO roles WHERE old.builtin DO INSTEAD nothing;") 78 | db.Exec("CREATE RULE builtin_del_rolebindings AS ON DELETE TO role_bindings WHERE builtin DO INSTEAD nothing;") 79 | db.Exec("CREATE RULE builtin_upd_rolebindings AS ON UPDATE TO role_bindings WHERE old.builtin DO INSTEAD nothing;") 80 | } 81 | -------------------------------------------------------------------------------- /controllers/audit.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strconv" 7 | 8 | "github.com/kriten-io/kriten/config" 9 | "github.com/kriten-io/kriten/middlewares" 10 | "github.com/kriten-io/kriten/services" 11 | 12 | "github.com/gin-gonic/gin" 13 | ) 14 | 15 | type AuditController struct { 16 | AuditService services.AuditService 17 | AuthService services.AuthService 18 | } 19 | 20 | func NewAuditController(als services.AuditService, as services.AuthService) AuditController { 21 | return AuditController{ 22 | AuditService: als, 23 | AuthService: as, 24 | } 25 | } 26 | 27 | func (ac *AuditController) SetAuditRoutes(rg *gin.RouterGroup, config config.Config) { 28 | r := rg.Group("").Use( 29 | middlewares.AuthenticationMiddleware(ac.AuthService, config.JWT)) 30 | 31 | r.Use(middlewares.AuthorizationMiddleware(ac.AuthService, "audit", "read")) 32 | { 33 | r.GET("", ac.ListAuditLogs) 34 | r.GET("/:id", ac.GetAuditLog) 35 | } 36 | } 37 | 38 | // ListAuditLogs godoc 39 | // 40 | // @Summary List audit 41 | // @Description List all audit logs 42 | // @Tags audit 43 | // @Accept json 44 | // @Produce json 45 | // @Success 200 {array} models.AuditLog 46 | // @Failure 400 {object} helpers.HTTPError 47 | // @Failure 404 {object} helpers.HTTPError 48 | // @Failure 500 {object} helpers.HTTPError 49 | // @Router /audit_logs [get] 50 | // @Security Bearer 51 | func (ac *AuditController) ListAuditLogs(ctx *gin.Context) { 52 | var err error 53 | // Default limit 54 | maxDefault := 100 55 | param := ctx.Request.URL.Query().Get("max") 56 | 57 | if param != "" { 58 | maxDefault, err = strconv.Atoi(param) 59 | if err != nil { 60 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 61 | return 62 | } 63 | } 64 | 65 | groups, err := ac.AuditService.ListAuditLogs(maxDefault) 66 | 67 | if err != nil { 68 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 69 | return 70 | } 71 | 72 | ctx.Header("Content-range", fmt.Sprintf("%v", len(groups))) 73 | if len(groups) == 0 { 74 | var arr [0]int 75 | ctx.JSON(http.StatusOK, arr) 76 | return 77 | } 78 | 79 | ctx.SetSameSite(http.SameSiteLaxMode) 80 | ctx.JSON(http.StatusOK, groups) 81 | } 82 | 83 | // GetAuditLog godoc 84 | // 85 | // @Summary Get audit log 86 | // @Description Get information about a specific audit log 87 | // @Tags audit 88 | // @Accept json 89 | // @Produce json 90 | // @Param id path string true "Audit Log ID" 91 | // @Success 200 {object} models.AuditLog 92 | // @Failure 400 {object} helpers.HTTPError 93 | // @Failure 404 {object} helpers.HTTPError 94 | // @Failure 500 {object} helpers.HTTPError 95 | // @Router /audit_logs/{id} [get] 96 | // @Security Bearer 97 | func (ac *AuditController) GetAuditLog(ctx *gin.Context) { 98 | auditLogID := ctx.Param("id") 99 | role, err := ac.AuditService.GetAuditLog(auditLogID) 100 | 101 | if err != nil { 102 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 103 | return 104 | } 105 | 106 | ctx.JSON(http.StatusOK, role) 107 | } 108 | -------------------------------------------------------------------------------- /controllers/auth.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/kriten-io/kriten/models" 8 | "github.com/kriten-io/kriten/services" 9 | 10 | "github.com/gin-gonic/gin" 11 | "golang.org/x/exp/slices" 12 | ) 13 | 14 | type AuthController struct { 15 | AuthService services.AuthService 16 | providers []string 17 | AuditService services.AuditService 18 | AuditCategory string 19 | } 20 | 21 | func NewAuthController(as services.AuthService, als services.AuditService, p []string) AuthController { 22 | return AuthController{ 23 | AuthService: as, 24 | AuditService: als, 25 | AuditCategory: "authentication", 26 | providers: p, 27 | } 28 | } 29 | 30 | func (ac *AuthController) SetAuthRoutes(rg *gin.RouterGroup) { 31 | rg.POST("/login", ac.Login) 32 | rg.GET("/refresh", ac.Refresh) 33 | } 34 | 35 | // Login godoc 36 | // 37 | // @Summary Authenticate users 38 | // @Description authenticate and generates a JWT token 39 | // @Tags authenticate 40 | // @Accept json 41 | // @Produce json 42 | // @Param credentials body models.Credentials true "Your credentials" 43 | // @Success 200 {object} string 44 | // @Failure 400 {object} helpers.HTTPError 45 | // @Failure 401 {object} helpers.HTTPError 46 | // @Failure 404 {object} helpers.HTTPError 47 | // @Failure 500 {object} helpers.HTTPError 48 | // @Router /login [post] 49 | func (ac *AuthController) Login(ctx *gin.Context) { 50 | // timestamp := time.Now().UTC() 51 | var credentials models.Credentials 52 | audit := ac.AuditService.InitialiseAuditLog(ctx, "login", ac.AuditCategory, "*") 53 | 54 | if err := ctx.ShouldBindJSON(&credentials); err != nil { 55 | ac.AuditService.CreateAudit(audit) 56 | ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 57 | return 58 | } 59 | 60 | audit.UserName = credentials.Username 61 | audit.Provider = credentials.Provider 62 | 63 | if !slices.Contains(ac.providers, credentials.Provider) { 64 | ac.AuditService.CreateAudit(audit) 65 | ctx.JSON(http.StatusBadRequest, gin.H{"error": "provider does not exist", "providers": ac.providers}) 66 | return 67 | } 68 | 69 | token, expiry, err := ac.AuthService.Login(&credentials) 70 | if err != nil { 71 | fmt.Println("error:", err) 72 | ac.AuditService.CreateAudit(audit) 73 | ctx.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials."}) 74 | return 75 | } 76 | 77 | audit.Status = "success" 78 | ac.AuditService.CreateAudit(audit) 79 | 80 | ctx.SetSameSite(http.SameSiteNoneMode) 81 | ctx.SetCookie("token", token, expiry, "", "", false, true) 82 | ctx.JSON(http.StatusOK, gin.H{"token": token}) 83 | } 84 | 85 | // Refresh godoc 86 | // 87 | // @Summary Auth admin 88 | // @Description Refresh time limit of a JWT token 89 | // @Tags authenticate 90 | // @Accept json 91 | // @Produce json 92 | // @Param token header string false "JWT Token can be provided as Cookie" 93 | // @Success 200 {object} string 94 | // @Failure 400 {object} helpers.HTTPError 95 | // @Failure 401 {object} helpers.HTTPError 96 | // @Failure 404 {object} helpers.HTTPError 97 | // @Failure 500 {object} helpers.HTTPError 98 | // @Router /refresh [get] 99 | // @Security Bearer 100 | func (ac *AuthController) Refresh(ctx *gin.Context) { 101 | token, err := ctx.Request.Cookie("token") 102 | if err == http.ErrNoCookie { 103 | ctx.JSON(http.StatusBadRequest, gin.H{"error": "please authenticate."}) 104 | return 105 | } 106 | 107 | newToken, expiry, err := ac.AuthService.Refresh(token.Value) 108 | if err != nil { 109 | ctx.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token."}) 110 | return 111 | } 112 | 113 | ctx.SetSameSite(http.SameSiteLaxMode) 114 | ctx.SetCookie("token", newToken, expiry, "", "", false, true) 115 | ctx.JSON(http.StatusOK, gin.H{"token": newToken}) 116 | } 117 | -------------------------------------------------------------------------------- /controllers/cronjobs.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/kriten-io/kriten/config" 8 | "github.com/kriten-io/kriten/middlewares" 9 | "github.com/kriten-io/kriten/models" 10 | "github.com/kriten-io/kriten/services" 11 | 12 | "github.com/gin-gonic/gin" 13 | ) 14 | 15 | type CronJobController struct { 16 | CronJobService services.CronJobService 17 | AuthService services.AuthService 18 | AuditService services.AuditService 19 | AuditCategory string 20 | } 21 | 22 | func NewCronJobController( 23 | js services.CronJobService, 24 | as services.AuthService, 25 | als services.AuditService, 26 | ) CronJobController { 27 | return CronJobController{ 28 | CronJobService: js, 29 | AuthService: as, 30 | AuditService: als, 31 | AuditCategory: "cronjobs", 32 | } 33 | } 34 | 35 | func (jc *CronJobController) SetCronJobRoutes(rg *gin.RouterGroup, config config.Config) { 36 | r := rg.Group("").Use( 37 | middlewares.AuthenticationMiddleware(jc.AuthService, config.JWT)) 38 | 39 | r.GET("", middlewares.SetAuthorizationListMiddleware(jc.AuthService, "cronjobs"), jc.ListCronJobs) 40 | r.GET("/:id", middlewares.AuthorizationMiddleware(jc.AuthService, "cronjobs", "read"), jc.GetCronJob) 41 | r.GET("/:id/schema", middlewares.AuthorizationMiddleware(jc.AuthService, "cronjobs", "read"), jc.GetSchema) 42 | 43 | r.Use(middlewares.AuthorizationMiddleware(jc.AuthService, "cronjobs", "write")) 44 | { 45 | r.POST("", jc.CreateCronJob) 46 | r.PUT("", jc.CreateCronJob) 47 | r.PATCH("/:id", jc.UpdateCronJob) 48 | r.PUT("/:id", jc.UpdateCronJob) 49 | r.DELETE("/:id", jc.DeleteCronJob) 50 | } 51 | 52 | } 53 | 54 | // ListCronJobs godoc 55 | // 56 | // @Summary List all Cronjobs 57 | // @Description List all Cronjobs 58 | // @Tags cronjobs 59 | // @Accept json 60 | // @Produce json 61 | // @Success 200 {array} models.CronJob 62 | // @Failure 400 {object} helpers.HTTPError 63 | // @Failure 404 {object} helpers.HTTPError 64 | // @Failure 500 {object} helpers.HTTPError 65 | // @Router /cronjobs [get] 66 | // @Security Bearer 67 | func (jc *CronJobController) ListCronJobs(ctx *gin.Context) { 68 | //audit := jc.AuditService.InitialiseAuditLog(ctx, "list", jc.AuditCategory, "*") 69 | authList := ctx.MustGet("authList").([]string) 70 | 71 | jobsList, err := jc.CronJobService.ListCronJobs(authList) 72 | 73 | if err != nil { 74 | //jc.AuditService.CreateAudit(audit) 75 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 76 | return 77 | } 78 | 79 | //audit.Status = "success" 80 | 81 | ctx.Header("Content-range", fmt.Sprintf("%v", len(jobsList))) 82 | if len(jobsList) == 0 { 83 | var arr [0]int 84 | //jc.AuditService.CreateAudit(audit) 85 | ctx.JSON(http.StatusOK, arr) 86 | return 87 | } 88 | 89 | //jc.AuditService.CreateAudit(audit) 90 | ctx.SetSameSite(http.SameSiteLaxMode) 91 | ctx.JSON(http.StatusOK, jobsList) 92 | } 93 | 94 | // GetCronJob godoc 95 | // 96 | // @Summary Get job info 97 | // @Description Get information about a specific job 98 | // @Tags cronjobs 99 | // @Accept json 100 | // @Produce json 101 | // @Param id path string true "CronJob id" 102 | // @Success 200 {object} models.CronJob 103 | // @Failure 400 {object} helpers.HTTPError 104 | // @Failure 404 {object} helpers.HTTPError 105 | // @Failure 500 {object} helpers.HTTPError 106 | // @Router /cronjobs/{id} [get] 107 | // @Security Bearer 108 | func (jc *CronJobController) GetCronJob(ctx *gin.Context) { 109 | jobName := ctx.Param("id") 110 | audit := jc.AuditService.InitialiseAuditLog(ctx, "get", jc.AuditCategory, jobName) 111 | job, err := jc.CronJobService.GetCronJob(jobName) 112 | 113 | if err != nil { 114 | jc.AuditService.CreateAudit(audit) 115 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 116 | return 117 | } 118 | 119 | audit.Status = "success" 120 | jc.AuditService.CreateAudit(audit) 121 | ctx.JSON(http.StatusOK, job) 122 | } 123 | 124 | // CreateCronJob godoc 125 | // 126 | // @Summary Create a new job 127 | // @Description Add a job to the cluster 128 | // @Tags cronjobs 129 | // @Accept json 130 | // @Produce json 131 | // @Param cronjob body models.CronJob true "New cronjob" 132 | // @Success 200 {object} models.CronJob 133 | // @Failure 400 {object} helpers.HTTPError 134 | // @Failure 404 {object} helpers.HTTPError 135 | // @Failure 500 {object} helpers.HTTPError 136 | // @Router /cronjobs [post] 137 | // @Security Bearer 138 | func (jc *CronJobController) CreateCronJob(ctx *gin.Context) { 139 | var cronjob models.CronJob 140 | audit := jc.AuditService.InitialiseAuditLog(ctx, "create", jc.AuditCategory, "*") 141 | username := ctx.MustGet("username").(string) 142 | 143 | if err := ctx.ShouldBindJSON(&cronjob); err != nil { 144 | jc.AuditService.CreateAudit(audit) 145 | ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 146 | return 147 | } 148 | audit.EventTarget = cronjob.Task 149 | 150 | cronjob.Owner = username 151 | job, err := jc.CronJobService.CreateCronJob(cronjob) 152 | 153 | if err != nil { 154 | jc.AuditService.CreateAudit(audit) 155 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 156 | return 157 | } 158 | 159 | audit.Status = "success" 160 | 161 | if job.Name != "" { 162 | jc.AuditService.CreateAudit(audit) 163 | ctx.JSON(http.StatusOK, job) 164 | return 165 | } 166 | 167 | jc.AuditService.CreateAudit(audit) 168 | ctx.JSON(http.StatusOK, gin.H{"msg": "job created successfully", "id": job.Name}) 169 | } 170 | 171 | // UpdateCronJob godoc 172 | // 173 | // @Summary Update a cronjob 174 | // @Description Update a cronjob in the cluster 175 | // @Tags cronjobs 176 | // @Accept json 177 | // @Produce json 178 | // @Param cronjob body models.CronJob true "Update CronJob" 179 | // @Success 200 {object} models.CronJob 180 | // @Failure 400 {object} helpers.HTTPError 181 | // @Failure 404 {object} helpers.HTTPError 182 | // @Failure 500 {object} helpers.HTTPError 183 | // @Router /cronjobs/ [patch] 184 | // @Security Bearer 185 | func (jc *CronJobController) UpdateCronJob(ctx *gin.Context) { 186 | var cronjob models.CronJob 187 | var err error 188 | id := ctx.Param("id") 189 | username := ctx.MustGet("username").(string) 190 | audit := jc.AuditService.InitialiseAuditLog(ctx, "update", jc.AuditCategory, id) 191 | 192 | if err := ctx.ShouldBindJSON(&cronjob); err != nil { 193 | jc.AuditService.CreateAudit(audit) 194 | ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 195 | return 196 | } 197 | 198 | cronjob.Owner = username 199 | cronjob, err = jc.CronJobService.UpdateCronJob(cronjob) 200 | if err != nil { 201 | jc.AuditService.CreateAudit(audit) 202 | ctx.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) 203 | return 204 | } 205 | audit.Status = "success" 206 | jc.AuditService.CreateAudit(audit) 207 | ctx.JSON(http.StatusOK, cronjob) 208 | } 209 | 210 | // DeleteCronJob godoc 211 | // 212 | // @Summary Delete a CronJob 213 | // @Description Delete by CronJob ID 214 | // @Tags cronjobs 215 | // @Accept json 216 | // @Produce json 217 | // @Param id path string true "CronJob ID" 218 | // @Success 204 {object} models.CronJob 219 | // @Failure 400 {object} helpers.HTTPError 220 | // @Failure 404 {object} helpers.HTTPError 221 | // @Failure 500 {object} helpers.HTTPError 222 | // @Router /cronjobs/{id} [delete] 223 | // @Security Bearer 224 | func (jc *CronJobController) DeleteCronJob(ctx *gin.Context) { 225 | groupID := ctx.Param("id") 226 | audit := jc.AuditService.InitialiseAuditLog(ctx, "delete", jc.AuditCategory, groupID) 227 | 228 | err := jc.CronJobService.DeleteCronJob(groupID) 229 | if err != nil { 230 | jc.AuditService.CreateAudit(audit) 231 | ctx.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) 232 | return 233 | } 234 | 235 | audit.Status = "success" 236 | jc.AuditService.CreateAudit(audit) 237 | ctx.JSON(http.StatusOK, gin.H{"msg": "cronjob deleted successfully"}) 238 | } 239 | 240 | // GetSchema godoc 241 | // 242 | // @Summary Get schema 243 | // @Description Get schema for the job info and input parameters 244 | // @Tags cronjobs 245 | // @Accept json 246 | // @Produce json 247 | // @Param id path string true "Task name" 248 | // @Success 200 {object} map[string]interface{} 249 | // @Failure 400 {object} helpers.HTTPError 250 | // @Failure 404 {object} helpers.HTTPError 251 | // @Failure 500 {object} helpers.HTTPError 252 | // @Router /cronjobs/{id}/schema [get] 253 | // @Security Bearer 254 | func (jc *CronJobController) GetSchema(ctx *gin.Context) { 255 | taskName := ctx.Param("id") 256 | audit := jc.AuditService.InitialiseAuditLog(ctx, "get_schema", jc.AuditCategory, taskName) 257 | schema, err := jc.CronJobService.GetSchema(taskName) 258 | 259 | if err != nil { 260 | jc.AuditService.CreateAudit(audit) 261 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 262 | return 263 | } 264 | audit.Status = "success" 265 | 266 | if schema == nil { 267 | jc.AuditService.CreateAudit(audit) 268 | ctx.JSON(http.StatusOK, gin.H{"msg": "schema not found"}) 269 | return 270 | } 271 | 272 | jc.AuditService.CreateAudit(audit) 273 | ctx.JSON(http.StatusOK, schema) 274 | } 275 | -------------------------------------------------------------------------------- /controllers/groups.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/kriten-io/kriten/config" 8 | "github.com/kriten-io/kriten/middlewares" 9 | "github.com/kriten-io/kriten/models" 10 | "github.com/kriten-io/kriten/services" 11 | uuid "github.com/satori/go.uuid" 12 | 13 | "github.com/gin-gonic/gin" 14 | "golang.org/x/exp/slices" 15 | ) 16 | 17 | type GroupController struct { 18 | GroupService services.GroupService 19 | AuthService services.AuthService 20 | AuditService services.AuditService 21 | AuditCategory string 22 | providers []string 23 | } 24 | 25 | func NewGroupController(groupService services.GroupService, 26 | as services.AuthService, 27 | als services.AuditService, p []string) GroupController { 28 | return GroupController{ 29 | GroupService: groupService, 30 | AuthService: as, 31 | providers: p, 32 | AuditService: als, 33 | AuditCategory: "groups", 34 | } 35 | } 36 | 37 | func (uc *GroupController) SetGroupRoutes(rg *gin.RouterGroup, config config.Config) { 38 | r := rg.Group("").Use( 39 | middlewares.AuthenticationMiddleware(uc.AuthService, config.JWT)) 40 | 41 | r.GET("", middlewares.SetAuthorizationListMiddleware(uc.AuthService, "groups"), uc.ListGroups) 42 | r.GET("/:id", middlewares.AuthorizationMiddleware(uc.AuthService, "groups", "read"), uc.GetGroup) 43 | 44 | r.Use(middlewares.AuthorizationMiddleware(uc.AuthService, "groups", "write")) 45 | { 46 | r.POST("", uc.CreateGroup) 47 | r.PUT("", uc.CreateGroup) 48 | r.PATCH("/:id", uc.UpdateGroup) 49 | r.PUT("/:id", uc.UpdateGroup) 50 | r.DELETE("/:id", uc.DeleteGroup) 51 | 52 | { 53 | r.GET("/:id/users", uc.ListUsersInGroup) 54 | r.POST("/:id/users", uc.AddUserToGroup) 55 | r.PUT("/:id/users", uc.AddUserToGroup) 56 | r.DELETE("/:id/users", uc.RemoveUserFromGroup) 57 | } 58 | } 59 | } 60 | 61 | // ListGroups godoc 62 | // 63 | // @Summary List all groups 64 | // @Description List all groups available on the cluster 65 | // @Tags groups 66 | // @Accept json 67 | // @Produce json 68 | // @Success 200 {array} models.Group 69 | // @Failure 400 {object} helpers.HTTPError 70 | // @Failure 404 {object} helpers.HTTPError 71 | // @Failure 500 {object} helpers.HTTPError 72 | // @Router /groups [get] 73 | // @Security Bearer 74 | func (gc *GroupController) ListGroups(ctx *gin.Context) { 75 | //audit := gc.AuditService.InitialiseAuditLog(ctx, "list", gc.AuditCategory, "*") 76 | authList := ctx.MustGet("authList").([]string) 77 | groups, err := gc.GroupService.ListGroups(authList) 78 | 79 | if err != nil { 80 | //gc.AuditService.CreateAudit(audit) 81 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 82 | return 83 | } 84 | 85 | //audit.Status = "success" 86 | 87 | ctx.Header("Content-range", fmt.Sprintf("%v", len(groups))) 88 | if len(groups) == 0 { 89 | var arr [0]int 90 | //gc.AuditService.CreateAudit(audit) 91 | ctx.JSON(http.StatusOK, arr) 92 | return 93 | } 94 | 95 | //gc.AuditService.CreateAudit(audit) 96 | ctx.SetSameSite(http.SameSiteLaxMode) 97 | ctx.JSON(http.StatusOK, groups) 98 | } 99 | 100 | // GetGroup godoc 101 | // 102 | // @Summary Get a group 103 | // @Description Get information about a specific group 104 | // @Tags groups 105 | // @Accept json 106 | // @Produce json 107 | // @Param id path string true "Group ID" 108 | // @Success 200 {object} models.Group 109 | // @Failure 400 {object} helpers.HTTPError 110 | // @Failure 404 {object} helpers.HTTPError 111 | // @Failure 500 {object} helpers.HTTPError 112 | // @Router /groups/{id} [get] 113 | // @Security Bearer 114 | func (gc *GroupController) GetGroup(ctx *gin.Context) { 115 | groupID := ctx.Param("id") 116 | audit := gc.AuditService.InitialiseAuditLog(ctx, "get", gc.AuditCategory, groupID) 117 | group, err := gc.GroupService.GetGroup(groupID) 118 | 119 | if err != nil { 120 | gc.AuditService.CreateAudit(audit) 121 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 122 | return 123 | } 124 | 125 | audit.Status = "success" 126 | 127 | gc.AuditService.CreateAudit(audit) 128 | ctx.JSON(http.StatusOK, group) 129 | } 130 | 131 | // CreateGroup godoc 132 | // 133 | // @Summary Create a new group 134 | // @Description Add a group to the cluster 135 | // @Tags groups 136 | // @Accept json 137 | // @Produce json 138 | // @Param group body models.Group true "New group" 139 | // @Success 200 {object} models.Group 140 | // @Failure 400 {object} helpers.HTTPError 141 | // @Failure 404 {object} helpers.HTTPError 142 | // @Failure 500 {object} helpers.HTTPError 143 | // @Router /groups [post] 144 | // @Security Bearer 145 | func (gc *GroupController) CreateGroup(ctx *gin.Context) { 146 | audit := gc.AuditService.InitialiseAuditLog(ctx, "create", gc.AuditCategory, "*") 147 | var group models.Group 148 | 149 | if err := ctx.ShouldBindJSON(&group); err != nil { 150 | gc.AuditService.CreateAudit(audit) 151 | ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 152 | return 153 | } 154 | 155 | audit.EventTarget = group.Name 156 | 157 | if !slices.Contains(gc.providers, group.Provider) { 158 | gc.AuditService.CreateAudit(audit) 159 | ctx.JSON(http.StatusBadRequest, gin.H{"error": "provider does not exist", "providers": gc.providers}) 160 | return 161 | } 162 | 163 | group, err := gc.GroupService.CreateGroup(group) 164 | if err != nil { 165 | gc.AuditService.CreateAudit(audit) 166 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 167 | return 168 | } 169 | 170 | audit.Status = "success" 171 | 172 | gc.AuditService.CreateAudit(audit) 173 | ctx.JSON(http.StatusOK, group) 174 | } 175 | 176 | // UpdateGroup godoc 177 | // 178 | // @Summary Update a group 179 | // @Description Update a group in the cluster 180 | // @Tags groups 181 | // @Accept json 182 | // @Produce json 183 | // @Param id path string true "Group ID" 184 | // @Param group body models.Group true "Update group" 185 | // @Success 200 {object} models.Group 186 | // @Failure 400 {object} helpers.HTTPError 187 | // @Failure 404 {object} helpers.HTTPError 188 | // @Failure 500 {object} helpers.HTTPError 189 | // @Router /groups/{id} [patch] 190 | // @Security Bearer 191 | func (gc *GroupController) UpdateGroup(ctx *gin.Context) { 192 | var group models.Group 193 | var err error 194 | groupID := ctx.Param("id") 195 | audit := gc.AuditService.InitialiseAuditLog(ctx, "update", gc.AuditCategory, groupID) 196 | 197 | if err := ctx.ShouldBindJSON(&group); err != nil { 198 | gc.AuditService.CreateAudit(audit) 199 | ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 200 | return 201 | } 202 | 203 | if !slices.Contains(gc.providers, group.Provider) { 204 | gc.AuditService.CreateAudit(audit) 205 | ctx.JSON(http.StatusBadRequest, gin.H{"error": "provider does not exist", "providers": gc.providers}) 206 | return 207 | } 208 | 209 | group.ID, err = uuid.FromString(groupID) 210 | if err != nil { 211 | gc.AuditService.CreateAudit(audit) 212 | ctx.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) 213 | return 214 | } 215 | 216 | group, err = gc.GroupService.UpdateGroup(group) 217 | if err != nil { 218 | gc.AuditService.CreateAudit(audit) 219 | ctx.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) 220 | return 221 | } 222 | audit.Status = "success" 223 | gc.AuditService.CreateAudit(audit) 224 | ctx.JSON(http.StatusOK, group) 225 | } 226 | 227 | // DeleteGroup godoc 228 | // 229 | // @Summary Delete a group 230 | // @Description Delete by group ID 231 | // @Tags groups 232 | // @Accept json 233 | // @Produce json 234 | // @Param id path string true "Group ID" 235 | // @Success 204 {object} models.Group 236 | // @Failure 400 {object} helpers.HTTPError 237 | // @Failure 404 {object} helpers.HTTPError 238 | // @Failure 500 {object} helpers.HTTPError 239 | // @Router /groups/{id} [delete] 240 | // @Security Bearer 241 | func (gc *GroupController) DeleteGroup(ctx *gin.Context) { 242 | groupID := ctx.Param("id") 243 | audit := gc.AuditService.InitialiseAuditLog(ctx, "delete", gc.AuditCategory, groupID) 244 | 245 | err := gc.GroupService.DeleteGroup(groupID) 246 | if err != nil { 247 | gc.AuditService.CreateAudit(audit) 248 | ctx.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) 249 | return 250 | } 251 | 252 | audit.Status = "success" 253 | gc.AuditService.CreateAudit(audit) 254 | ctx.JSON(http.StatusOK, gin.H{"msg": "group deleted successfully"}) 255 | } 256 | 257 | // ListUsersInGroup godoc 258 | // 259 | // @Summary List users 260 | // @Description List all users in given group 261 | // @Tags groups 262 | // @Accept json 263 | // @Produce json 264 | // @Param id path string true "Group ID" 265 | // @Success 200 {array} []models.GroupUser 266 | // @Failure 400 {object} helpers.HTTPError 267 | // @Failure 404 {object} helpers.HTTPError 268 | // @Failure 500 {object} helpers.HTTPError 269 | // @Router /groups/{id}/users [get] 270 | // @Security Bearer 271 | func (gc *GroupController) ListUsersInGroup(ctx *gin.Context) { 272 | id := ctx.Param("id") 273 | // audit := gc.AuditService.InitialiseAuditLog(ctx, "list_users", gc.AuditCategory, id) 274 | var err error 275 | 276 | users, err := gc.GroupService.ListUsersInGroup(id) 277 | if err != nil { 278 | // gc.AuditService.CreateAudit(audit) 279 | ctx.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) 280 | return 281 | } 282 | 283 | //audit.Status = "success" 284 | 285 | ctx.Header("Content-range", fmt.Sprintf("%v", len(users))) 286 | if len(users) == 0 { 287 | var arr [0]int 288 | //gc.AuditService.CreateAudit(audit) 289 | ctx.JSON(http.StatusOK, arr) 290 | return 291 | } 292 | 293 | // gc.AuditService.CreateAudit(audit) 294 | ctx.JSON(http.StatusOK, users) 295 | } 296 | 297 | // AddUserToGroup godoc 298 | // 299 | // @Summary Add users 300 | // @Description Add users to group 301 | // @Tags groups 302 | // @Accept json 303 | // @Produce json 304 | // @Param group body []models.GroupUser true "Users to be added" 305 | // @Param id path string true "Group ID" 306 | // @Success 200 {object} models.Group 307 | // @Failure 400 {object} helpers.HTTPError 308 | // @Failure 404 {object} helpers.HTTPError 309 | // @Failure 500 {object} helpers.HTTPError 310 | // @Router /groups/{id}/users [post] 311 | // @Security Bearer 312 | func (gc *GroupController) AddUserToGroup(ctx *gin.Context) { 313 | id := ctx.Param("id") 314 | audit := gc.AuditService.InitialiseAuditLog(ctx, "add_users", gc.AuditCategory, id) 315 | var users []models.GroupUser 316 | var err error 317 | 318 | if err := ctx.ShouldBindJSON(&users); err != nil { 319 | gc.AuditService.CreateAudit(audit) 320 | ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 321 | return 322 | } 323 | 324 | group, err := gc.GroupService.AddUsersToGroup(id, users) 325 | if err != nil { 326 | gc.AuditService.CreateAudit(audit) 327 | ctx.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) 328 | return 329 | } 330 | audit.Status = "success" 331 | gc.AuditService.CreateAudit(audit) 332 | ctx.JSON(http.StatusOK, group) 333 | } 334 | 335 | // RemoveUserFromGroup godoc 336 | // 337 | // @Summary Remove users 338 | // @Description Remove users from group 339 | // @Tags groups 340 | // @Accept json 341 | // @Produce json 342 | // @Param group body []models.GroupUser true "Users to be removed" 343 | // @Param id path string true "Group ID" 344 | // @Success 200 {object} models.Group 345 | // @Failure 400 {object} helpers.HTTPError 346 | // @Failure 404 {object} helpers.HTTPError 347 | // @Failure 500 {object} helpers.HTTPError 348 | // @Router /groups/{id}/users [delete] 349 | // @Security Bearer 350 | func (gc *GroupController) RemoveUserFromGroup(ctx *gin.Context) { 351 | id := ctx.Param("id") 352 | audit := gc.AuditService.InitialiseAuditLog(ctx, "remove_users", gc.AuditCategory, id) 353 | var users []models.GroupUser 354 | var err error 355 | 356 | if err := ctx.ShouldBindJSON(&users); err != nil { 357 | gc.AuditService.CreateAudit(audit) 358 | ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 359 | return 360 | } 361 | 362 | group, err := gc.GroupService.RemoveUsersFromGroup(id, users) 363 | if err != nil { 364 | gc.AuditService.CreateAudit(audit) 365 | ctx.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) 366 | return 367 | } 368 | audit.Status = "success" 369 | gc.AuditService.CreateAudit(audit) 370 | ctx.JSON(http.StatusOK, group) 371 | } 372 | -------------------------------------------------------------------------------- /controllers/job.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | 8 | "github.com/kriten-io/kriten/config" 9 | "github.com/kriten-io/kriten/middlewares" 10 | "github.com/kriten-io/kriten/services" 11 | 12 | "github.com/gin-gonic/gin" 13 | ) 14 | 15 | type JobController struct { 16 | JobService services.JobService 17 | AuthService services.AuthService 18 | AuditService services.AuditService 19 | AuditCategory string 20 | } 21 | 22 | func NewJobController(js services.JobService, as services.AuthService, als services.AuditService) JobController { 23 | return JobController{ 24 | JobService: js, 25 | AuthService: as, 26 | AuditService: als, 27 | AuditCategory: "jobs", 28 | } 29 | } 30 | 31 | func (jc *JobController) SetJobRoutes(rg *gin.RouterGroup, config config.Config) { 32 | r := rg.Group("").Use( 33 | middlewares.AuthenticationMiddleware(jc.AuthService, config.JWT)) 34 | 35 | r.GET("", middlewares.SetAuthorizationListMiddleware(jc.AuthService, "jobs"), jc.ListJobs) 36 | r.GET("/:id", middlewares.AuthorizationMiddleware(jc.AuthService, "jobs", "read"), jc.GetJob) 37 | r.GET("/:id/log", middlewares.AuthorizationMiddleware(jc.AuthService, "jobs", "read"), jc.GetJobLog) 38 | r.GET("/:id/schema", middlewares.AuthorizationMiddleware(jc.AuthService, "jobs", "read"), jc.GetSchema) 39 | 40 | r.Use(middlewares.AuthorizationMiddleware(jc.AuthService, "jobs", "write")) 41 | { 42 | r.POST(":id", jc.CreateJob) 43 | r.PUT(":id", jc.CreateJob) 44 | } 45 | 46 | } 47 | 48 | // ListJobs godoc 49 | // 50 | // @Summary List all jobs 51 | // @Description List all jobs 52 | // @Tags jobs 53 | // @Accept json 54 | // @Produce json 55 | // @Success 200 {array} string 56 | // @Failure 400 {object} helpers.HTTPError 57 | // @Failure 404 {object} helpers.HTTPError 58 | // @Failure 500 {object} helpers.HTTPError 59 | // @Router /jobs [get] 60 | // @Security Bearer 61 | func (jc *JobController) ListJobs(ctx *gin.Context) { 62 | // audit := jc.AuditService.InitialiseAuditLog(ctx, "list", jc.AuditCategory, "*") 63 | authList := ctx.MustGet("authList").([]string) 64 | 65 | jobsList, err := jc.JobService.ListJobs(authList) 66 | 67 | if err != nil { 68 | // jc.AuditService.CreateAudit(audit) 69 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 70 | return 71 | } 72 | 73 | // audit.Status = "success" 74 | 75 | ctx.Header("Content-range", fmt.Sprintf("%v", len(jobsList))) 76 | if len(jobsList) == 0 { 77 | var arr [0]int 78 | // jc.AuditService.CreateAudit(audit) 79 | ctx.JSON(http.StatusOK, arr) 80 | return 81 | } 82 | 83 | //jc.AuditService.CreateAudit(audit) 84 | ctx.SetSameSite(http.SameSiteLaxMode) 85 | ctx.JSON(http.StatusOK, jobsList) 86 | } 87 | 88 | // GetJob godoc 89 | // 90 | // @Summary Get job info 91 | // @Description Get information about a specific job 92 | // @Tags jobs 93 | // @Accept json 94 | // @Produce json 95 | // @Param id path string true "Job id" 96 | // @Success 200 {object} models.Task 97 | // @Failure 400 {object} helpers.HTTPError 98 | // @Failure 404 {object} helpers.HTTPError 99 | // @Failure 500 {object} helpers.HTTPError 100 | // @Router /jobs/{id} [get] 101 | // @Security Bearer 102 | func (jc *JobController) GetJob(ctx *gin.Context) { 103 | username := ctx.MustGet("username").(string) 104 | jobName := ctx.Param("id") 105 | // audit := jc.AuditService.InitialiseAuditLog(ctx, "get", jc.AuditCategory, jobName) 106 | job, err := jc.JobService.GetJob(username, jobName) 107 | 108 | if err != nil { 109 | // jc.AuditService.CreateAudit(audit) 110 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 111 | return 112 | } 113 | 114 | // audit.Status = "success" 115 | // jc.AuditService.CreateAudit(audit) 116 | ctx.JSON(http.StatusOK, job) 117 | } 118 | 119 | // GetJobLog godoc 120 | // 121 | // @Summary Get a job log 122 | // @Description Get a job log as text 123 | // @Tags jobs 124 | // @Accept json 125 | // @Produce json 126 | // @Param id path string true "Job id" 127 | // @Success 200 {object} models.Task 128 | // @Failure 400 {object} helpers.HTTPError 129 | // @Failure 404 {object} helpers.HTTPError 130 | // @Failure 500 {object} helpers.HTTPError 131 | // @Router /jobs/{id}/log [get] 132 | // @Security Bearer 133 | func (jc *JobController) GetJobLog(ctx *gin.Context) { 134 | username := ctx.MustGet("username").(string) 135 | jobName := ctx.Param("id") 136 | // audit := jc.AuditService.InitialiseAuditLog(ctx, "get_job_log", jc.AuditCategory, jobName) 137 | log, err := jc.JobService.GetLog(username, jobName) 138 | 139 | if err != nil { 140 | // jc.AuditService.CreateAudit(audit) 141 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 142 | return 143 | } 144 | 145 | // audit.Status = "success" 146 | 147 | // jc.AuditService.CreateAudit(audit) 148 | ctx.Data(http.StatusOK, "text/plain", []byte(log)) 149 | } 150 | 151 | // CreateJob godoc 152 | // 153 | // @Summary Create a new job 154 | // @Description Add a job to the cluster 155 | // @Tags jobs 156 | // @Accept json 157 | // @Produce json 158 | // @Param id path string true "Task name" 159 | // @Param evars body object false "Extra vars" 160 | // @Success 200 {object} models.Task 161 | // @Failure 400 {object} helpers.HTTPError 162 | // @Failure 404 {object} helpers.HTTPError 163 | // @Failure 500 {object} helpers.HTTPError 164 | // @Router /jobs/{id} [post] 165 | // @Security Bearer 166 | func (jc *JobController) CreateJob(ctx *gin.Context) { 167 | taskID := ctx.Param("id") 168 | audit := jc.AuditService.InitialiseAuditLog(ctx, "create", jc.AuditCategory, taskID) 169 | username := ctx.MustGet("username").(string) 170 | 171 | extraVars, err := io.ReadAll(ctx.Request.Body) 172 | 173 | if err != nil { 174 | jc.AuditService.CreateAudit(audit) 175 | ctx.JSON(http.StatusBadRequest, gin.H{"error": err}) 176 | return 177 | } 178 | 179 | job, err := jc.JobService.CreateJob(username, taskID, string(extraVars)) 180 | 181 | if err != nil { 182 | jc.AuditService.CreateAudit(audit) 183 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 184 | return 185 | } 186 | 187 | audit.Status = "success" 188 | 189 | if (job.ID != "") && (job.Completed != 0) { 190 | //ctx.JSON(http.StatusOK, gin.H{"id": jobID, "json_data": sync.JsonData}) 191 | jc.AuditService.CreateAudit(audit) 192 | ctx.JSON(http.StatusOK, job) 193 | return 194 | } 195 | 196 | jc.AuditService.CreateAudit(audit) 197 | ctx.JSON(http.StatusOK, gin.H{"msg": "job created successfully", "id": job.ID}) 198 | } 199 | 200 | // GetSchema godoc 201 | // 202 | // @Summary Get task schema 203 | // @Description Get task schema for the job info and input parameters 204 | // @Tags jobs 205 | // @Accept json 206 | // @Produce json 207 | // @Param id path string true "Task name" 208 | // @Success 200 {object} map[string]interface{} 209 | // @Failure 400 {object} helpers.HTTPError 210 | // @Failure 404 {object} helpers.HTTPError 211 | // @Failure 500 {object} helpers.HTTPError 212 | // @Router /jobs/{id}/schema [get] 213 | // @Security Bearer 214 | func (jc *JobController) GetSchema(ctx *gin.Context) { 215 | taskName := ctx.Param("id") 216 | // audit := jc.AuditService.InitialiseAuditLog(ctx, "get_schema", jc.AuditCategory, taskName) 217 | schema, err := jc.JobService.GetSchema(taskName) 218 | 219 | if err != nil { 220 | // jc.AuditService.CreateAudit(audit) 221 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 222 | return 223 | } 224 | // audit.Status = "success" 225 | 226 | if schema == nil { 227 | // jc.AuditService.CreateAudit(audit) 228 | ctx.JSON(http.StatusOK, gin.H{"msg": "schema not found"}) 229 | return 230 | } 231 | 232 | // jc.AuditService.CreateAudit(audit) 233 | ctx.JSON(http.StatusOK, schema) 234 | } 235 | -------------------------------------------------------------------------------- /controllers/role_bindings.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/kriten-io/kriten/config" 8 | "github.com/kriten-io/kriten/middlewares" 9 | "github.com/kriten-io/kriten/models" 10 | "github.com/kriten-io/kriten/services" 11 | uuid "github.com/satori/go.uuid" 12 | 13 | "github.com/gin-gonic/gin" 14 | "golang.org/x/exp/slices" 15 | ) 16 | 17 | // TODO: This is currently hardcoded but needs to be fetched from somewhere else 18 | var subjectKinds = []string{"groups"} 19 | 20 | type RoleBindingController struct { 21 | RoleBindingService services.RoleBindingService 22 | AuthService services.AuthService 23 | AuditService services.AuditService 24 | AuditCategory string 25 | providers []string 26 | } 27 | 28 | func NewRoleBindingController(rbs services.RoleBindingService, as services.AuthService, als services.AuditService, p []string) RoleBindingController { 29 | return RoleBindingController{ 30 | RoleBindingService: rbs, 31 | AuthService: as, 32 | providers: p, 33 | AuditService: als, 34 | AuditCategory: "groups", 35 | } 36 | } 37 | 38 | func (rc *RoleBindingController) SetRoleBindingRoutes(rg *gin.RouterGroup, config config.Config) { 39 | r := rg.Group("").Use( 40 | middlewares.AuthenticationMiddleware(rc.AuthService, config.JWT)) 41 | 42 | r.GET("", middlewares.SetAuthorizationListMiddleware(rc.AuthService, "role_bindings"), rc.ListRoleBindings) 43 | r.GET("/:id", middlewares.AuthorizationMiddleware(rc.AuthService, "role_bindings", "read"), rc.GetRoleBinding) 44 | 45 | r.Use(middlewares.AuthorizationMiddleware(rc.AuthService, "role_bindings", "write")) 46 | { 47 | r.POST("", rc.CreateRoleBinding) 48 | r.PUT("", rc.CreateRoleBinding) 49 | r.PATCH("/:id", rc.UpdateRoleBinding) 50 | r.PUT("/:id", rc.UpdateRoleBinding) 51 | r.DELETE("/:id", rc.DeleteRoleBinding) 52 | } 53 | } 54 | 55 | // ListRoleBindings godoc 56 | // 57 | // @Summary List all role bindings 58 | // @Description List all roles bindings available on the cluster 59 | // @Tags rolebindings 60 | // @Accept json 61 | // @Produce json 62 | // @Success 200 {array} models.RoleBinding 63 | // @Failure 400 {object} helpers.HTTPError 64 | // @Failure 404 {object} helpers.HTTPError 65 | // @Failure 500 {object} helpers.HTTPError 66 | // @Router /role_bindings [get] 67 | // @Security Bearer 68 | func (rc *RoleBindingController) ListRoleBindings(ctx *gin.Context) { 69 | //audit := rc.AuditService.InitialiseAuditLog(ctx, "list", rc.AuditCategory, "*") 70 | filters := make(map[string]string) 71 | authList := ctx.MustGet("authList").([]string) 72 | 73 | urlParams := ctx.Request.URL.Query() 74 | 75 | // urlParams contains a map[string][]string 76 | // we need to parse it into a map[string]string 77 | // so we will only take the first value 78 | for key, value := range urlParams { 79 | filters[key] = value[0] 80 | } 81 | 82 | roles, err := rc.RoleBindingService.ListRoleBindings(authList, filters) 83 | 84 | if err != nil { 85 | //rc.AuditService.CreateAudit(audit) 86 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 87 | return 88 | } 89 | 90 | //audit.Status = "success" 91 | ctx.Header("Content-range", fmt.Sprintf("%v", len(roles))) 92 | if len(roles) == 0 { 93 | var arr [0]int 94 | //rc.AuditService.CreateAudit(audit) 95 | ctx.JSON(http.StatusOK, arr) 96 | return 97 | } 98 | 99 | //rc.AuditService.CreateAudit(audit) 100 | ctx.SetSameSite(http.SameSiteLaxMode) 101 | ctx.JSON(http.StatusOK, roles) 102 | } 103 | 104 | // GetRoleBinding godoc 105 | // 106 | // @Summary Get a role binding 107 | // @Description Get information about a specific role binding 108 | // @Tags rolebindings 109 | // @Accept json 110 | // @Produce json 111 | // @Param id path string true "RoleBinding ID" 112 | // @Success 200 {object} models.RoleBinding 113 | // @Failure 400 {object} helpers.HTTPError 114 | // @Failure 404 {object} helpers.HTTPError 115 | // @Failure 500 {object} helpers.HTTPError 116 | // @Router /role_bindings/{id} [get] 117 | // @Security Bearer 118 | func (rc *RoleBindingController) GetRoleBinding(ctx *gin.Context) { 119 | roleBindingID := ctx.Param("id") 120 | audit := rc.AuditService.InitialiseAuditLog(ctx, "get", rc.AuditCategory, roleBindingID) 121 | role, err := rc.RoleBindingService.GetRoleBinding(roleBindingID) 122 | 123 | if err != nil { 124 | rc.AuditService.CreateAudit(audit) 125 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 126 | return 127 | } 128 | audit.Status = "success" 129 | 130 | rc.AuditService.CreateAudit(audit) 131 | ctx.JSON(http.StatusOK, role) 132 | } 133 | 134 | // CreateRoleBinding godoc 135 | // 136 | // @Summary Create a new role binding 137 | // @Description Add a role binding to the cluster 138 | // @Tags rolebindings 139 | // @Accept json 140 | // @Produce json 141 | // @Param roleBinding body models.RoleBinding true "New role binding" 142 | // @Success 200 {object} models.RoleBinding 143 | // @Failure 400 {object} helpers.HTTPError 144 | // @Failure 404 {object} helpers.HTTPError 145 | // @Failure 500 {object} helpers.HTTPError 146 | // @Router /role_bindings [post] 147 | // @Security Bearer 148 | func (rc *RoleBindingController) CreateRoleBinding(ctx *gin.Context) { 149 | audit := rc.AuditService.InitialiseAuditLog(ctx, "create", rc.AuditCategory, "*") 150 | var roleBinding models.RoleBinding 151 | 152 | if err := ctx.ShouldBindJSON(&roleBinding); err != nil { 153 | rc.AuditService.CreateAudit(audit) 154 | ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 155 | return 156 | } 157 | 158 | audit.EventTarget = roleBinding.Name 159 | 160 | if !slices.Contains(subjectKinds, roleBinding.SubjectKind) { 161 | rc.AuditService.CreateAudit(audit) 162 | ctx.JSON(http.StatusBadRequest, gin.H{"error": "subject kind does not exist", "subject_kinds": subjectKinds}) 163 | return 164 | } 165 | if !slices.Contains(rc.providers, roleBinding.SubjectProvider) { 166 | rc.AuditService.CreateAudit(audit) 167 | ctx.JSON(http.StatusBadRequest, gin.H{"error": "provider does not exist", "providers": rc.providers}) 168 | return 169 | } 170 | 171 | rolebinding, err := rc.RoleBindingService.CreateRoleBinding(roleBinding) 172 | if err != nil { 173 | rc.AuditService.CreateAudit(audit) 174 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 175 | return 176 | } 177 | 178 | audit.Status = "success" 179 | rc.AuditService.CreateAudit(audit) 180 | ctx.JSON(http.StatusOK, rolebinding) 181 | } 182 | 183 | // UpdateRoleBinding godoc 184 | // 185 | // @Summary Update a role binding 186 | // @Description Update a role binding in the cluster 187 | // @Tags rolebindings 188 | // @Accept json 189 | // @Produce json 190 | // @Param id path string true "RoleBinding ID" 191 | // @Param role body models.RoleBinding true "Update role" 192 | // @Success 200 {object} models.RoleBinding 193 | // @Failure 400 {object} helpers.HTTPError 194 | // @Failure 404 {object} helpers.HTTPError 195 | // @Failure 500 {object} helpers.HTTPError 196 | // @Router /role_bindings/{id} [patch] 197 | // @Security Bearer 198 | func (rc *RoleBindingController) UpdateRoleBinding(ctx *gin.Context) { 199 | roleBindingID := ctx.Param("id") 200 | audit := rc.AuditService.InitialiseAuditLog(ctx, "update", rc.AuditCategory, roleBindingID) 201 | var roleBinding models.RoleBinding 202 | var err error 203 | 204 | if err := ctx.ShouldBindJSON(&roleBinding); err != nil { 205 | rc.AuditService.CreateAudit(audit) 206 | ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 207 | return 208 | } 209 | 210 | if !slices.Contains(subjectKinds, roleBinding.SubjectKind) { 211 | rc.AuditService.CreateAudit(audit) 212 | ctx.JSON(http.StatusBadRequest, gin.H{"error": "subject kind does not exist", "subject_kinds": subjectKinds}) 213 | return 214 | } 215 | if !slices.Contains(rc.providers, roleBinding.SubjectProvider) { 216 | rc.AuditService.CreateAudit(audit) 217 | ctx.JSON(http.StatusBadRequest, gin.H{"error": "provider does not exist", "providers": rc.providers}) 218 | return 219 | } 220 | 221 | roleBinding.ID, err = uuid.FromString(roleBindingID) 222 | if err != nil { 223 | rc.AuditService.CreateAudit(audit) 224 | ctx.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) 225 | return 226 | } 227 | 228 | roleBinding, err = rc.RoleBindingService.UpdateRoleBinding(roleBinding) 229 | if err != nil { 230 | rc.AuditService.CreateAudit(audit) 231 | ctx.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) 232 | return 233 | } 234 | audit.Status = "success" 235 | rc.AuditService.CreateAudit(audit) 236 | ctx.JSON(http.StatusOK, roleBinding) 237 | } 238 | 239 | // DeleteRoleBinding godoc 240 | // 241 | // @Summary Delete a role binding 242 | // @Description Delete by role binding ID 243 | // @Tags rolebindings 244 | // @Accept json 245 | // @Produce json 246 | // @Param id path string true "RoleBinding ID" 247 | // @Success 204 {object} models.RoleBinding 248 | // @Failure 400 {object} helpers.HTTPError 249 | // @Failure 404 {object} helpers.HTTPError 250 | // @Failure 500 {object} helpers.HTTPError 251 | // @Router /role_bindings/{id} [delete] 252 | // @Security Bearer 253 | func (rc *RoleBindingController) DeleteRoleBinding(ctx *gin.Context) { 254 | roleBindingID := ctx.Param("id") 255 | audit := rc.AuditService.InitialiseAuditLog(ctx, "delete", rc.AuditCategory, roleBindingID) 256 | 257 | err := rc.RoleBindingService.DeleteRoleBinding(roleBindingID) 258 | if err != nil { 259 | rc.AuditService.CreateAudit(audit) 260 | ctx.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) 261 | return 262 | } 263 | audit.Status = "success" 264 | rc.AuditService.CreateAudit(audit) 265 | ctx.JSON(http.StatusOK, gin.H{"msg": "role binding deleted successfully"}) 266 | } 267 | -------------------------------------------------------------------------------- /controllers/roles.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/kriten-io/kriten/config" 8 | "github.com/kriten-io/kriten/middlewares" 9 | "github.com/kriten-io/kriten/models" 10 | "github.com/kriten-io/kriten/services" 11 | uuid "github.com/satori/go.uuid" 12 | 13 | "github.com/gin-gonic/gin" 14 | "golang.org/x/exp/slices" 15 | ) 16 | 17 | // TODO: This is currently hardcoded but needs to be fetched from somewhere else 18 | var resources = []string{"runners", "tasks", "jobs", "users", "roles", "role_bindings"} 19 | var access = []string{"read", "write"} 20 | 21 | type RoleController struct { 22 | RoleService services.RoleService 23 | AuthService services.AuthService 24 | AuditService services.AuditService 25 | AuditCategory string 26 | } 27 | 28 | func NewRoleController(rs services.RoleService, as services.AuthService, als services.AuditService) RoleController { 29 | return RoleController{ 30 | RoleService: rs, 31 | AuthService: as, 32 | AuditService: als, 33 | AuditCategory: "roles", 34 | } 35 | } 36 | 37 | func (rc *RoleController) SetRoleRoutes(rg *gin.RouterGroup, config config.Config) { 38 | r := rg.Group("").Use( 39 | middlewares.AuthenticationMiddleware(rc.AuthService, config.JWT)) 40 | 41 | r.GET("", middlewares.SetAuthorizationListMiddleware(rc.AuthService, "roles"), rc.ListRoles) 42 | r.GET("/:id", middlewares.AuthorizationMiddleware(rc.AuthService, "roles", "read"), rc.GetRole) 43 | 44 | r.Use(middlewares.AuthorizationMiddleware(rc.AuthService, "roles", "write")) 45 | { 46 | r.POST("", rc.CreateRole) 47 | r.PUT("", rc.CreateRole) 48 | r.PATCH("/:id", rc.UpdateRole) 49 | r.PUT("/:id", rc.UpdateRole) 50 | r.DELETE("/:id", rc.DeleteRole) 51 | } 52 | } 53 | 54 | // ListRoles godoc 55 | // 56 | // @Summary List all roles 57 | // @Description List all roles available on the cluster 58 | // @Tags roles 59 | // @Accept json 60 | // @Produce json 61 | // @Success 200 {array} models.Role 62 | // @Failure 400 {object} helpers.HTTPError 63 | // @Failure 404 {object} helpers.HTTPError 64 | // @Failure 500 {object} helpers.HTTPError 65 | // @Router /roles [get] 66 | // @Security Bearer 67 | func (rc *RoleController) ListRoles(ctx *gin.Context) { 68 | //audit := rc.AuditService.InitialiseAuditLog(ctx, "list", rc.AuditCategory, "*") 69 | authList := ctx.MustGet("authList").([]string) 70 | roles, err := rc.RoleService.ListRoles(authList) 71 | 72 | if err != nil { 73 | //rc.AuditService.CreateAudit(audit) 74 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 75 | return 76 | } 77 | 78 | //audit.Status = "success" 79 | ctx.Header("Content-range", fmt.Sprintf("%v", len(roles))) 80 | if len(roles) == 0 { 81 | var arr [0]int 82 | //rc.AuditService.CreateAudit(audit) 83 | ctx.JSON(http.StatusOK, arr) 84 | return 85 | } 86 | 87 | //rc.AuditService.CreateAudit(audit) 88 | ctx.SetSameSite(http.SameSiteLaxMode) 89 | ctx.JSON(http.StatusOK, roles) 90 | } 91 | 92 | // GetRole godoc 93 | // 94 | // @Summary Get a role 95 | // @Description Get information about a specific role 96 | // @Tags roles 97 | // @Accept json 98 | // @Produce json 99 | // @Param id path string true "Role ID" 100 | // @Success 200 {object} models.Role 101 | // @Failure 400 {object} helpers.HTTPError 102 | // @Failure 404 {object} helpers.HTTPError 103 | // @Failure 500 {object} helpers.HTTPError 104 | // @Router /roles/{id} [get] 105 | // @Security Bearer 106 | func (rc *RoleController) GetRole(ctx *gin.Context) { 107 | roleID := ctx.Param("id") 108 | audit := rc.AuditService.InitialiseAuditLog(ctx, "get", rc.AuditCategory, roleID) 109 | role, err := rc.RoleService.GetRole(roleID) 110 | 111 | if err != nil { 112 | rc.AuditService.CreateAudit(audit) 113 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 114 | return 115 | } 116 | 117 | audit.Status = "success" 118 | rc.AuditService.CreateAudit(audit) 119 | ctx.JSON(http.StatusOK, role) 120 | } 121 | 122 | // CreateRole godoc 123 | // 124 | // @Summary Create a new role 125 | // @Description Add a role to the cluster 126 | // @Tags roles 127 | // @Accept json 128 | // @Produce json 129 | // @Param role body models.Role true "New role" 130 | // @Success 200 {object} models.Role 131 | // @Failure 400 {object} helpers.HTTPError 132 | // @Failure 404 {object} helpers.HTTPError 133 | // @Failure 500 {object} helpers.HTTPError 134 | // @Router /roles [post] 135 | // @Security Bearer 136 | func (rc *RoleController) CreateRole(ctx *gin.Context) { 137 | audit := rc.AuditService.InitialiseAuditLog(ctx, "create", rc.AuditCategory, "*") 138 | var role models.Role 139 | 140 | if err := ctx.ShouldBindJSON(&role); err != nil { 141 | rc.AuditService.CreateAudit(audit) 142 | ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 143 | return 144 | } 145 | audit.EventTarget = role.Name 146 | 147 | if !slices.Contains(resources, role.Resource) { 148 | rc.AuditService.CreateAudit(audit) 149 | ctx.JSON(http.StatusBadRequest, gin.H{"error": "resource does not exist", "resources": resources}) 150 | return 151 | } 152 | if !slices.Contains(access, role.Access) { 153 | rc.AuditService.CreateAudit(audit) 154 | ctx.JSON(http.StatusBadRequest, gin.H{"error": "access not allowed", "access": access}) 155 | return 156 | } 157 | 158 | role, err := rc.RoleService.CreateRole(role) 159 | if err != nil { 160 | rc.AuditService.CreateAudit(audit) 161 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 162 | return 163 | } 164 | 165 | audit.Status = "success" 166 | rc.AuditService.CreateAudit(audit) 167 | ctx.JSON(http.StatusOK, role) 168 | } 169 | 170 | // UpdateRole godoc 171 | // 172 | // @Summary Update a role 173 | // @Description Update a role in the cluster 174 | // @Tags roles 175 | // @Accept json 176 | // @Produce json 177 | // @Param id path string true "Role ID" 178 | // @Param role body models.Role true "Update role" 179 | // @Success 200 {object} models.Role 180 | // @Failure 400 {object} helpers.HTTPError 181 | // @Failure 404 {object} helpers.HTTPError 182 | // @Failure 500 {object} helpers.HTTPError 183 | // @Router /roles/{id} [patch] 184 | // @Security Bearer 185 | func (rc *RoleController) UpdateRole(ctx *gin.Context) { 186 | roleID := ctx.Param("id") 187 | audit := rc.AuditService.InitialiseAuditLog(ctx, "update", rc.AuditCategory, roleID) 188 | var role models.Role 189 | var err error 190 | 191 | if err := ctx.ShouldBindJSON(&role); err != nil { 192 | rc.AuditService.CreateAudit(audit) 193 | ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 194 | return 195 | } 196 | 197 | role.ID, err = uuid.FromString(roleID) 198 | if err != nil { 199 | rc.AuditService.CreateAudit(audit) 200 | ctx.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) 201 | return 202 | } 203 | 204 | role, err = rc.RoleService.UpdateRole(role) 205 | if err != nil { 206 | rc.AuditService.CreateAudit(audit) 207 | ctx.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) 208 | return 209 | } 210 | audit.Status = "success" 211 | rc.AuditService.CreateAudit(audit) 212 | ctx.JSON(http.StatusOK, role) 213 | } 214 | 215 | // DeleteRole godoc 216 | // 217 | // @Summary Delete a role 218 | // @Description Delete by role ID 219 | // @Tags roles 220 | // @Accept json 221 | // @Produce json 222 | // @Param id path string true "Role ID" 223 | // @Success 204 {object} models.Role 224 | // @Failure 400 {object} helpers.HTTPError 225 | // @Failure 404 {object} helpers.HTTPError 226 | // @Failure 500 {object} helpers.HTTPError 227 | // @Router /roles/{id} [delete] 228 | // @Security Bearer 229 | func (rc *RoleController) DeleteRole(ctx *gin.Context) { 230 | roleID := ctx.Param("id") 231 | audit := rc.AuditService.InitialiseAuditLog(ctx, "delete", rc.AuditCategory, roleID) 232 | 233 | err := rc.RoleService.DeleteRole(roleID) 234 | if err != nil { 235 | rc.AuditService.CreateAudit(audit) 236 | ctx.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) 237 | return 238 | } 239 | audit.Status = "success" 240 | rc.AuditService.CreateAudit(audit) 241 | ctx.JSON(http.StatusOK, gin.H{"msg": "role deleted successfully"}) 242 | } 243 | -------------------------------------------------------------------------------- /controllers/task.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "strings" 8 | 9 | "github.com/kriten-io/kriten/config" 10 | "github.com/kriten-io/kriten/middlewares" 11 | "github.com/kriten-io/kriten/models" 12 | "github.com/kriten-io/kriten/services" 13 | 14 | "github.com/gin-gonic/gin" 15 | "k8s.io/apimachinery/pkg/api/errors" 16 | ) 17 | 18 | type TaskController struct { 19 | TaskService services.TaskService 20 | AuthService services.AuthService 21 | AuditService services.AuditService 22 | AuditCategory string 23 | } 24 | 25 | func NewTaskController(taskservice services.TaskService, as services.AuthService, als services.AuditService) TaskController { 26 | return TaskController{ 27 | TaskService: taskservice, 28 | AuthService: as, 29 | AuditService: als, 30 | AuditCategory: "tasks", 31 | } 32 | } 33 | 34 | func (tc *TaskController) SetTaskRoutes(rg *gin.RouterGroup, config config.Config) { 35 | r := rg.Group("").Use( 36 | middlewares.AuthenticationMiddleware(tc.AuthService, config.JWT)) 37 | 38 | r.GET("", middlewares.SetAuthorizationListMiddleware(tc.AuthService, "tasks"), tc.ListTasks) 39 | r.GET("/:id", middlewares.AuthorizationMiddleware(tc.AuthService, "tasks", "read"), tc.GetTask) 40 | 41 | r.Use(middlewares.AuthorizationMiddleware(tc.AuthService, "tasks", "write")) 42 | { 43 | r.POST("", tc.CreateTask) 44 | r.PUT("", tc.CreateTask) 45 | r.PATCH("/:id", tc.UpdateTask) 46 | r.PUT("/:id", tc.UpdateTask) 47 | r.DELETE("/:id", tc.DeleteTask) 48 | 49 | { 50 | r.GET("/:id/schema", tc.GetSchema) 51 | r.POST("/:id/schema", tc.UpdateSchema) 52 | r.PUT("/:id/schema", tc.UpdateSchema) 53 | r.DELETE("/:id/schema", tc.DeleteSchema) 54 | } 55 | } 56 | 57 | } 58 | 59 | // ListTask godoc 60 | // 61 | // @Summary List all tasks 62 | // @Description List all tasks available on the cluster 63 | // @Tags tasks 64 | // @Accept json 65 | // @Produce json 66 | // @Success 200 {array} models.Task 67 | // @Failure 400 {object} helpers.HTTPError 68 | // @Failure 404 {object} helpers.HTTPError 69 | // @Failure 500 {object} helpers.HTTPError 70 | // @Router /tasks [get] 71 | // @Security Bearer 72 | func (tc *TaskController) ListTasks(ctx *gin.Context) { 73 | //audit := tc.AuditService.InitialiseAuditLog(ctx, "list", tc.AuditCategory, "*") 74 | authList := ctx.MustGet("authList").([]string) 75 | 76 | tasks, err := tc.TaskService.ListTasks(authList) 77 | 78 | if err != nil { 79 | //tc.AuditService.CreateAudit(audit) 80 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 81 | return 82 | } 83 | 84 | //audit.Status = "success" 85 | ctx.Header("Content-range", fmt.Sprintf("%v", len(tasks))) 86 | if len(tasks) == 0 { 87 | var arr [0]int 88 | //tc.AuditService.CreateAudit(audit) 89 | ctx.JSON(http.StatusOK, arr) 90 | return 91 | } 92 | 93 | // ctx.Header("Content-range", fmt.Sprintf("%v", len(tasksList))) 94 | //tc.AuditService.CreateAudit(audit) 95 | ctx.JSON(http.StatusOK, tasks) 96 | // ctx.JSON(http.StatusOK, gin.H{"msg": "tasks list retrieved successfully", "tasks": tasksList}) 97 | } 98 | 99 | // GetTask godoc 100 | // 101 | // @Summary Get a task 102 | // @Description Get information about a specific task 103 | // @Tags tasks 104 | // @Accept json 105 | // @Produce json 106 | // @Param id path string true "Task name" 107 | // @Success 200 {object} models.Task 108 | // @Failure 400 {object} helpers.HTTPError 109 | // @Failure 404 {object} helpers.HTTPError 110 | // @Failure 500 {object} helpers.HTTPError 111 | // @Router /tasks/{id} [get] 112 | // @Security Bearer 113 | func (tc *TaskController) GetTask(ctx *gin.Context) { 114 | taskName := ctx.Param("id") 115 | audit := tc.AuditService.InitialiseAuditLog(ctx, "get", tc.AuditCategory, taskName) 116 | // username := ctx.MustGet("username").(string) 117 | task, err := tc.TaskService.GetTask(taskName) 118 | 119 | if err != nil { 120 | tc.AuditService.CreateAudit(audit) 121 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 122 | return 123 | } 124 | 125 | if task == nil { 126 | tc.AuditService.CreateAudit(audit) 127 | ctx.JSON(http.StatusOK, gin.H{"msg": "task not found"}) 128 | return 129 | } 130 | audit.Status = "success" 131 | 132 | // ctx.JSON(http.StatusOK, gin.H{"msg": "task retrieved successfully", "value": task, "secret": secret}) 133 | tc.AuditService.CreateAudit(audit) 134 | ctx.JSON(http.StatusOK, task) 135 | } 136 | 137 | // CreateTask godoc 138 | // 139 | // @Summary Create a new task 140 | // @Description Add a task to the cluster 141 | // @Tags tasks 142 | // @Accept json 143 | // @Produce json 144 | // @Param task body models.Task true "New task" 145 | // @Success 200 {object} models.Task 146 | // @Failure 400 {object} helpers.HTTPError 147 | // @Failure 404 {object} helpers.HTTPError 148 | // @Failure 500 {object} helpers.HTTPError 149 | // @Router /tasks [post] 150 | // @Security Bearer 151 | func (tc *TaskController) CreateTask(ctx *gin.Context) { 152 | audit := tc.AuditService.InitialiseAuditLog(ctx, "create", tc.AuditCategory, "*") 153 | var task models.Task 154 | 155 | if err := ctx.ShouldBindJSON(&task); err != nil { 156 | tc.AuditService.CreateAudit(audit) 157 | ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 158 | return 159 | } 160 | audit.EventTarget = task.Name 161 | 162 | taskConfig, err := tc.TaskService.CreateTask(task) 163 | if err != nil { 164 | switch { 165 | case errors.IsAlreadyExists(err): 166 | tc.AuditService.CreateAudit(audit) 167 | ctx.JSON(http.StatusConflict, gin.H{"error": "task already exists, please use a different name"}) 168 | return 169 | case strings.Contains(err.Error(), "invalid runner name"): 170 | tc.AuditService.CreateAudit(audit) 171 | ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 172 | return 173 | default: 174 | tc.AuditService.CreateAudit(audit) 175 | ctx.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) 176 | return 177 | } 178 | } 179 | 180 | audit.Status = "success" 181 | tc.AuditService.CreateAudit(audit) 182 | ctx.JSON(http.StatusOK, taskConfig) 183 | } 184 | 185 | // UpdateTask godoc 186 | // 187 | // @Summary Update a task 188 | // @Description Update a task in the cluster 189 | // @Tags tasks 190 | // @Accept json 191 | // @Produce json 192 | // @Param id path string true "Task name" 193 | // @Param task body models.Task true "Update task" 194 | // @Success 200 {object} models.Task 195 | // @Failure 400 {object} helpers.HTTPError 196 | // @Failure 404 {object} helpers.HTTPError 197 | // @Failure 500 {object} helpers.HTTPError 198 | // @Router /tasks/{id} [patch] 199 | // @Security Bearer 200 | func (tc *TaskController) UpdateTask(ctx *gin.Context) { 201 | taskName := ctx.Param("id") 202 | audit := tc.AuditService.InitialiseAuditLog(ctx, "update", tc.AuditCategory, taskName) 203 | var task models.Task 204 | 205 | if err := ctx.ShouldBindJSON(&task); err != nil { 206 | tc.AuditService.CreateAudit(audit) 207 | ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 208 | return 209 | } 210 | 211 | taskConfig, err := tc.TaskService.UpdateTask(task) 212 | if err != nil { 213 | if errors.IsNotFound(err) { 214 | tc.AuditService.CreateAudit(audit) 215 | ctx.JSON(http.StatusConflict, gin.H{"error": "task doesn't exist"}) 216 | return 217 | } 218 | tc.AuditService.CreateAudit(audit) 219 | ctx.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) 220 | return 221 | } 222 | audit.Status = "success" 223 | tc.AuditService.CreateAudit(audit) 224 | ctx.JSON(http.StatusOK, taskConfig) 225 | } 226 | 227 | // DeleteTask godoc 228 | // 229 | // @Summary Delete a task 230 | // @Description Delete by task name 231 | // @Tags tasks 232 | // @Accept json 233 | // @Produce json 234 | // @Param id path string true "Task name" 235 | // @Success 204 {object} models.Task 236 | // @Failure 400 {object} helpers.HTTPError 237 | // @Failure 404 {object} helpers.HTTPError 238 | // @Failure 500 {object} helpers.HTTPError 239 | // @Router /tasks/{id} [delete] 240 | // @Security Bearer 241 | func (tc *TaskController) DeleteTask(ctx *gin.Context) { 242 | taskName := ctx.Param("id") 243 | audit := tc.AuditService.InitialiseAuditLog(ctx, "delete", tc.AuditCategory, taskName) 244 | 245 | err := tc.TaskService.DeleteTask(taskName) 246 | if err != nil { 247 | if errors.IsNotFound(err) { 248 | tc.AuditService.CreateAudit(audit) 249 | ctx.JSON(http.StatusConflict, gin.H{"error": "task doesn't exist"}) 250 | return 251 | } 252 | tc.AuditService.CreateAudit(audit) 253 | ctx.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) 254 | return 255 | } 256 | audit.Status = "success" 257 | tc.AuditService.CreateAudit(audit) 258 | ctx.JSON(http.StatusOK, gin.H{"msg": "task deleted successfully"}) 259 | } 260 | 261 | // GetSchema godoc 262 | // 263 | // @Summary Get schema 264 | // @Description Get validation schema associated to a specific task 265 | // @Tags tasks 266 | // @Accept json 267 | // @Produce json 268 | // @Param id path string true "Task name" 269 | // @Success 200 {object} map[string]interface{} 270 | // @Failure 400 {object} helpers.HTTPError 271 | // @Failure 404 {object} helpers.HTTPError 272 | // @Failure 500 {object} helpers.HTTPError 273 | // @Router /tasks/{id}/schema [get] 274 | // @Security Bearer 275 | func (tc *TaskController) GetSchema(ctx *gin.Context) { 276 | taskName := ctx.Param("id") 277 | audit := tc.AuditService.InitialiseAuditLog(ctx, "get_schema", tc.AuditCategory, taskName) 278 | schema, err := tc.TaskService.GetSchema(taskName) 279 | 280 | if err != nil { 281 | tc.AuditService.CreateAudit(audit) 282 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 283 | return 284 | } 285 | audit.Status = "success" 286 | 287 | if schema == nil { 288 | tc.AuditService.CreateAudit(audit) 289 | ctx.JSON(http.StatusOK, gin.H{"msg": "schema not found"}) 290 | return 291 | } 292 | 293 | tc.AuditService.CreateAudit(audit) 294 | ctx.JSON(http.StatusOK, schema) 295 | } 296 | 297 | // UpdateSchema godoc 298 | // 299 | // @Summary Update schema 300 | // @Description Add or Update validation schema associated to a specific task 301 | // @Tags tasks 302 | // @Accept json 303 | // @Produce json 304 | // @Param id path string true "Task name" 305 | // @Param schema body map[string]interface{} true "New schema" 306 | // @Success 200 {object} map[string]interface{} 307 | // @Failure 400 {object} helpers.HTTPError 308 | // @Failure 404 {object} helpers.HTTPError 309 | // @Failure 500 {object} helpers.HTTPError 310 | // @Router /tasks/{id}/schema [post] 311 | // @Security Bearer 312 | func (tc *TaskController) UpdateSchema(ctx *gin.Context) { 313 | taskName := ctx.Param("id") 314 | audit := tc.AuditService.InitialiseAuditLog(ctx, "update_schema", tc.AuditCategory, taskName) 315 | var schema map[string]interface{} 316 | 317 | if err := ctx.BindJSON(&schema); err != nil { 318 | log.Println(err) 319 | tc.AuditService.CreateAudit(audit) 320 | ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 321 | return 322 | } 323 | 324 | schema, err := tc.TaskService.UpdateSchema(taskName, schema) 325 | if err != nil { 326 | tc.AuditService.CreateAudit(audit) 327 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 328 | return 329 | } 330 | audit.Status = "success" 331 | tc.AuditService.CreateAudit(audit) 332 | ctx.JSON(http.StatusOK, schema) 333 | } 334 | 335 | // DeleteSchema godoc 336 | // 337 | // @Summary Delete schema 338 | // @Description Remove validation schema associated to a specific task 339 | // @Tags tasks 340 | // @Accept json 341 | // @Produce json 342 | // @Param id path string true "Task name" 343 | // @Success 200 {object} map[string]interface{} 344 | // @Failure 400 {object} helpers.HTTPError 345 | // @Failure 404 {object} helpers.HTTPError 346 | // @Failure 500 {object} helpers.HTTPError 347 | // @Router /tasks/{id}/schema [delete] 348 | // @Security Bearer 349 | func (tc *TaskController) DeleteSchema(ctx *gin.Context) { 350 | taskName := ctx.Param("id") 351 | audit := tc.AuditService.InitialiseAuditLog(ctx, "delete_schema", tc.AuditCategory, taskName) 352 | 353 | err := tc.TaskService.DeleteSchema(taskName) 354 | 355 | if err != nil { 356 | tc.AuditService.CreateAudit(audit) 357 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 358 | return 359 | } 360 | 361 | audit.Status = "success" 362 | tc.AuditService.CreateAudit(audit) 363 | ctx.JSON(http.StatusOK, gin.H{"msg": "schema deleted successfully"}) 364 | } 365 | -------------------------------------------------------------------------------- /controllers/tokens.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/kriten-io/kriten/config" 8 | "github.com/kriten-io/kriten/middlewares" 9 | "github.com/kriten-io/kriten/models" 10 | "github.com/kriten-io/kriten/services" 11 | 12 | "github.com/gin-gonic/gin" 13 | uuid "github.com/satori/go.uuid" 14 | ) 15 | 16 | type ApiTokenController struct { 17 | ApiTokenService services.ApiTokenService 18 | AuthService services.AuthService 19 | providers []string 20 | AuditService services.AuditService 21 | AuditCategory string 22 | } 23 | 24 | func NewApiTokenController(apiTokenService services.ApiTokenService, as services.AuthService, als services.AuditService, p []string) ApiTokenController { 25 | return ApiTokenController{ 26 | ApiTokenService: apiTokenService, 27 | AuthService: as, 28 | providers: p, 29 | AuditService: als, 30 | AuditCategory: "apiTokens", 31 | } 32 | } 33 | 34 | func (uc *ApiTokenController) SetApiTokenRoutes(rg *gin.RouterGroup, config config.Config) { 35 | r := rg.Group("").Use( 36 | middlewares.AuthenticationMiddleware(uc.AuthService, config.JWT)) 37 | 38 | // Authorizations is set in the svc, only returning own tokens 39 | r.GET("", uc.ListApiTokens) 40 | 41 | r.GET("/all", middlewares.SetAuthorizationListMiddleware(uc.AuthService, "apiTokens"), uc.ListAllApiTokens) 42 | r.GET("/:id", middlewares.AuthorizationMiddleware(uc.AuthService, "apiTokens", "read"), uc.GetApiToken) 43 | 44 | r.POST("", uc.CreateApiToken) 45 | r.PUT("", uc.CreateApiToken) 46 | 47 | r.Use(middlewares.AuthorizationMiddleware(uc.AuthService, "apiTokens", "write")) 48 | { 49 | r.PATCH("/:id", uc.UpdateApiToken) 50 | r.PUT("/:id", uc.UpdateApiToken) 51 | r.DELETE("/:id", uc.DeleteApiToken) 52 | } 53 | } 54 | 55 | // ListApiTokens godoc 56 | // 57 | // @Summary List own apiTokens 58 | // @Description List own apiTokens available on the cluster 59 | // @Tags api_tokens 60 | // @Accept json 61 | // @Produce json 62 | // @Success 200 {array} models.ApiToken 63 | // @Failure 400 {object} helpers.HTTPError 64 | // @Failure 404 {object} helpers.HTTPError 65 | // @Failure 500 {object} helpers.HTTPError 66 | // @Router /api_tokens [get] 67 | // @Security Bearer 68 | func (uc *ApiTokenController) ListApiTokens(ctx *gin.Context) { 69 | // audit := uc.AuditService.InitialiseAuditLog(ctx, "list", uc.AuditCategory, "*") 70 | userid := ctx.MustGet("userID").(uuid.UUID) 71 | apiTokens, err := uc.ApiTokenService.ListApiTokens(userid) 72 | 73 | if err != nil { 74 | // uc.AuditService.CreateAudit(audit) 75 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 76 | return 77 | } 78 | 79 | // audit.Status = "success" 80 | ctx.Header("Content-range", fmt.Sprintf("%v", len(apiTokens))) 81 | if len(apiTokens) == 0 { 82 | var arr [0]int 83 | // uc.AuditService.CreateAudit(audit) 84 | ctx.JSON(http.StatusOK, arr) 85 | return 86 | } 87 | 88 | // uc.AuditService.CreateAudit(audit) 89 | ctx.SetSameSite(http.SameSiteLaxMode) 90 | ctx.JSON(http.StatusOK, apiTokens) 91 | } 92 | 93 | // ListAllApiTokens godoc 94 | // 95 | // @Summary List all apiTokens 96 | // @Description List all apiTokens available on the cluster 97 | // @Tags api_tokens 98 | // @Accept json 99 | // @Produce json 100 | // @Success 200 {array} models.ApiToken 101 | // @Failure 400 {object} helpers.HTTPError 102 | // @Failure 404 {object} helpers.HTTPError 103 | // @Failure 500 {object} helpers.HTTPError 104 | // @Router /api_tokens/all [get] 105 | // @Security Bearer 106 | func (uc *ApiTokenController) ListAllApiTokens(ctx *gin.Context) { 107 | // audit := uc.AuditService.InitialiseAuditLog(ctx, "list", uc.AuditCategory, "*") 108 | authList := ctx.MustGet("authList").([]string) 109 | apiTokens, err := uc.ApiTokenService.ListAllApiTokens(authList) 110 | 111 | if err != nil { 112 | // uc.AuditService.CreateAudit(audit) 113 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 114 | return 115 | } 116 | 117 | // audit.Status = "success" 118 | ctx.Header("Content-range", fmt.Sprintf("%v", len(apiTokens))) 119 | if len(apiTokens) == 0 { 120 | var arr [0]int 121 | // uc.AuditService.CreateAudit(audit) 122 | ctx.JSON(http.StatusOK, arr) 123 | return 124 | } 125 | 126 | // uc.AuditService.CreateAudit(audit) 127 | ctx.SetSameSite(http.SameSiteLaxMode) 128 | ctx.JSON(http.StatusOK, apiTokens) 129 | } 130 | 131 | // GetApiToken godoc 132 | // 133 | // @Summary Get a apiToken 134 | // @Description Get information about a specific apiToken 135 | // @Tags api_tokens 136 | // @Accept json 137 | // @Produce json 138 | // @Param id path string true "ApiToken ID" 139 | // @Success 200 {object} models.ApiToken 140 | // @Failure 400 {object} helpers.HTTPError 141 | // @Failure 404 {object} helpers.HTTPError 142 | // @Failure 500 {object} helpers.HTTPError 143 | // @Router /api_tokens/{id} [get] 144 | // @Security Bearer 145 | func (uc *ApiTokenController) GetApiToken(ctx *gin.Context) { 146 | apiTokenID := ctx.Param("id") 147 | // audit := uc.AuditService.InitialiseAuditLog(ctx, "get", uc.AuditCategory, apiTokenID) 148 | apiToken, err := uc.ApiTokenService.GetApiToken(apiTokenID) 149 | 150 | if err != nil { 151 | // uc.AuditService.CreateAudit(audit) 152 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 153 | return 154 | } 155 | 156 | // audit.Status = "success" 157 | // uc.AuditService.CreateAudit(audit) 158 | ctx.JSON(http.StatusOK, apiToken) 159 | } 160 | 161 | // CreateApiToken godoc 162 | // 163 | // @Summary Create a new apiToken 164 | // @Description Add a apiToken to the cluster 165 | // @Tags api_tokens 166 | // @Accept json 167 | // @Produce json 168 | // @Param apiToken body models.ApiToken true "New apiToken" 169 | // @Success 200 {object} models.ApiToken 170 | // @Failure 400 {object} helpers.HTTPError 171 | // @Failure 404 {object} helpers.HTTPError 172 | // @Failure 500 {object} helpers.HTTPError 173 | // @Router /api_tokens [post] 174 | // @Security Bearer 175 | func (atc *ApiTokenController) CreateApiToken(ctx *gin.Context) { 176 | userid := ctx.MustGet("userID").(uuid.UUID) 177 | audit := atc.AuditService.InitialiseAuditLog(ctx, "create", atc.AuditCategory, "*") 178 | var apiToken models.ApiToken 179 | 180 | if err := ctx.ShouldBindJSON(&apiToken); err != nil { 181 | atc.AuditService.CreateAudit(audit) 182 | ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 183 | return 184 | } 185 | audit.EventTarget = apiToken.Key 186 | apiToken.Owner = userid 187 | 188 | apiToken, err := atc.ApiTokenService.CreateApiToken(apiToken) 189 | if err != nil { 190 | atc.AuditService.CreateAudit(audit) 191 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 192 | return 193 | } 194 | if audit.EventTarget == "" { 195 | audit.EventTarget = apiToken.Key 196 | } 197 | 198 | audit.Status = "success" 199 | ctx.JSON(http.StatusOK, apiToken) 200 | } 201 | 202 | // UpdateApiToken godoc 203 | // 204 | // @Summary Update a apiToken 205 | // @Description Update a apiToken in the cluster 206 | // @Tags api_tokens 207 | // @Accept json 208 | // @Produce json 209 | // @Param id path string true "ApiToken ID" 210 | // @Param apiToken body models.ApiToken true "Update apiToken" 211 | // @Success 200 {object} models.ApiToken 212 | // @Failure 400 {object} helpers.HTTPError 213 | // @Failure 404 {object} helpers.HTTPError 214 | // @Failure 500 {object} helpers.HTTPError 215 | // @Router /api_tokens/{id} [patch] 216 | // @Security Bearer 217 | func (uc *ApiTokenController) UpdateApiToken(ctx *gin.Context) { 218 | apiTokenID := ctx.Param("id") 219 | audit := uc.AuditService.InitialiseAuditLog(ctx, "update", uc.AuditCategory, apiTokenID) 220 | var apiToken models.ApiToken 221 | var err error 222 | 223 | if err := ctx.ShouldBindJSON(&apiToken); err != nil { 224 | uc.AuditService.CreateAudit(audit) 225 | ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 226 | return 227 | } 228 | 229 | apiToken.ID, err = uuid.FromString(apiTokenID) 230 | if err != nil { 231 | uc.AuditService.CreateAudit(audit) 232 | ctx.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) 233 | return 234 | } 235 | 236 | apiToken, err = uc.ApiTokenService.UpdateApiToken(apiToken) 237 | if err != nil { 238 | uc.AuditService.CreateAudit(audit) 239 | ctx.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) 240 | return 241 | } 242 | audit.Status = "success" 243 | uc.AuditService.CreateAudit(audit) 244 | ctx.JSON(http.StatusOK, apiToken) 245 | } 246 | 247 | // DeleteApiToken godoc 248 | // 249 | // @Summary Delete a apiToken 250 | // @Description Delete by apiToken ID 251 | // @Tags api_tokens 252 | // @Accept json 253 | // @Produce json 254 | // @Param id path string true "ApiToken ID" 255 | // @Success 204 {object} models.ApiToken 256 | // @Failure 400 {object} helpers.HTTPError 257 | // @Failure 404 {object} helpers.HTTPError 258 | // @Failure 500 {object} helpers.HTTPError 259 | // @Router /api_tokens/{id} [delete] 260 | // @Security Bearer 261 | func (uc *ApiTokenController) DeleteApiToken(ctx *gin.Context) { 262 | apiTokenID := ctx.Param("id") 263 | audit := uc.AuditService.InitialiseAuditLog(ctx, "delete", uc.AuditCategory, apiTokenID) 264 | 265 | err := uc.ApiTokenService.DeleteApiToken(apiTokenID) 266 | if err != nil { 267 | uc.AuditService.CreateAudit(audit) 268 | ctx.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) 269 | return 270 | } 271 | audit.Status = "success" 272 | uc.AuditService.CreateAudit(audit) 273 | ctx.JSON(http.StatusOK, gin.H{"msg": "apiToken deleted successfully"}) 274 | } 275 | -------------------------------------------------------------------------------- /controllers/users.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/kriten-io/kriten/config" 8 | "github.com/kriten-io/kriten/middlewares" 9 | "github.com/kriten-io/kriten/models" 10 | "github.com/kriten-io/kriten/services" 11 | uuid "github.com/satori/go.uuid" 12 | 13 | "github.com/gin-gonic/gin" 14 | "golang.org/x/exp/slices" 15 | ) 16 | 17 | type UserController struct { 18 | UserService services.UserService 19 | GroupService services.GroupService 20 | AuthService services.AuthService 21 | providers []string 22 | AuditService services.AuditService 23 | AuditCategory string 24 | } 25 | 26 | func NewUserController(userService services.UserService, gs services.GroupService, as services.AuthService, als services.AuditService, p []string) UserController { 27 | return UserController{ 28 | UserService: userService, 29 | GroupService: gs, 30 | AuthService: as, 31 | providers: p, 32 | AuditService: als, 33 | AuditCategory: "users", 34 | } 35 | } 36 | 37 | func (uc *UserController) SetUserRoutes(rg *gin.RouterGroup, config config.Config) { 38 | r := rg.Group("").Use( 39 | middlewares.AuthenticationMiddleware(uc.AuthService, config.JWT)) 40 | 41 | r.GET("", middlewares.SetAuthorizationListMiddleware(uc.AuthService, "users"), uc.ListUsers) 42 | r.GET("/:id", middlewares.AuthorizationMiddleware(uc.AuthService, "users", "read"), uc.GetUser) 43 | r.GET("/:id/groups", middlewares.AuthorizationMiddleware(uc.AuthService, "users", "read"), uc.GetUserGroups) 44 | 45 | r.Use(middlewares.AuthorizationMiddleware(uc.AuthService, "users", "write")) 46 | { 47 | r.POST("", uc.CreateUser) 48 | r.PUT("", uc.CreateUser) 49 | r.PATCH("/:id", uc.UpdateUser) 50 | r.PUT("/:id", uc.UpdateUser) 51 | r.DELETE("/:id", uc.DeleteUser) 52 | } 53 | } 54 | 55 | // ListUsers godoc 56 | // 57 | // @Summary List all users 58 | // @Description List all users available on the cluster 59 | // @Tags users 60 | // @Accept json 61 | // @Produce json 62 | // @Success 200 {array} models.User 63 | // @Failure 400 {object} helpers.HTTPError 64 | // @Failure 404 {object} helpers.HTTPError 65 | // @Failure 500 {object} helpers.HTTPError 66 | // @Router /users [get] 67 | // @Security Bearer 68 | func (uc *UserController) ListUsers(ctx *gin.Context) { 69 | // audit := uc.AuditService.InitialiseAuditLog(ctx, "list", uc.AuditCategory, "*") 70 | authList := ctx.MustGet("authList").([]string) 71 | users, err := uc.UserService.ListUsers(authList) 72 | 73 | if err != nil { 74 | // uc.AuditService.CreateAudit(audit) 75 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 76 | return 77 | } 78 | 79 | // audit.Status = "success" 80 | ctx.Header("Content-range", fmt.Sprintf("%v", len(users))) 81 | if len(users) == 0 { 82 | var arr [0]int 83 | // uc.AuditService.CreateAudit(audit) 84 | ctx.JSON(http.StatusOK, arr) 85 | return 86 | } 87 | 88 | // uc.AuditService.CreateAudit(audit) 89 | ctx.SetSameSite(http.SameSiteLaxMode) 90 | ctx.JSON(http.StatusOK, users) 91 | } 92 | 93 | // GetUser godoc 94 | // 95 | // @Summary Get a user 96 | // @Description Get information about a specific user 97 | // @Tags users 98 | // @Accept json 99 | // @Produce json 100 | // @Param id path string true "User ID" 101 | // @Success 200 {object} models.User 102 | // @Failure 400 {object} helpers.HTTPError 103 | // @Failure 404 {object} helpers.HTTPError 104 | // @Failure 500 {object} helpers.HTTPError 105 | // @Router /users/{id} [get] 106 | // @Security Bearer 107 | func (uc *UserController) GetUser(ctx *gin.Context) { 108 | userID := ctx.Param("id") 109 | // audit := uc.AuditService.InitialiseAuditLog(ctx, "list", uc.AuditCategory, userID) 110 | user, err := uc.UserService.GetUser(userID) 111 | 112 | if err != nil { 113 | // uc.AuditService.CreateAudit(audit) 114 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 115 | return 116 | } 117 | user.Groups = []string{} 118 | // audit.Status = "success" 119 | // uc.AuditService.CreateAudit(audit) 120 | ctx.JSON(http.StatusOK, user) 121 | } 122 | 123 | func (uc *UserController) GetUserGroups(ctx *gin.Context) { 124 | userID := ctx.Param("id") 125 | // audit := uc.AuditService.InitialiseAuditLog(ctx, "list", uc.AuditCategory, userID) 126 | groups, err := uc.GroupService.GetUserGroups(userID) 127 | 128 | if err != nil { 129 | // uc.AuditService.CreateAudit(audit) 130 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 131 | return 132 | } 133 | 134 | // audit.Status = "success" 135 | // uc.AuditService.CreateAudit(audit) 136 | ctx.JSON(http.StatusOK, groups) 137 | } 138 | 139 | // CreateUser godoc 140 | // 141 | // @Summary Create a new user 142 | // @Description Add a user to the cluster 143 | // @Tags users 144 | // @Accept json 145 | // @Produce json 146 | // @Param user body models.User true "New user" 147 | // @Success 200 {object} models.User 148 | // @Failure 400 {object} helpers.HTTPError 149 | // @Failure 404 {object} helpers.HTTPError 150 | // @Failure 500 {object} helpers.HTTPError 151 | // @Router /users [post] 152 | // @Security Bearer 153 | func (uc *UserController) CreateUser(ctx *gin.Context) { 154 | audit := uc.AuditService.InitialiseAuditLog(ctx, "list", uc.AuditCategory, "*") 155 | var user models.User 156 | 157 | if err := ctx.ShouldBindJSON(&user); err != nil { 158 | uc.AuditService.CreateAudit(audit) 159 | ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 160 | return 161 | } 162 | audit.EventTarget = user.Username 163 | 164 | if !slices.Contains(uc.providers, user.Provider) { 165 | uc.AuditService.CreateAudit(audit) 166 | ctx.JSON(http.StatusBadRequest, gin.H{"error": "provider does not exist", "providers": uc.providers}) 167 | return 168 | } 169 | 170 | user, err := uc.UserService.CreateUser(user) 171 | if err != nil { 172 | uc.AuditService.CreateAudit(audit) 173 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 174 | return 175 | } 176 | 177 | audit.Status = "success" 178 | ctx.JSON(http.StatusOK, user) 179 | } 180 | 181 | // UpdateUser godoc 182 | // 183 | // @Summary Update a user 184 | // @Description Update a user in the cluster 185 | // @Tags users 186 | // @Accept json 187 | // @Produce json 188 | // @Param id path string true "User ID" 189 | // @Param user body models.User true "Update user" 190 | // @Success 200 {object} models.User 191 | // @Failure 400 {object} helpers.HTTPError 192 | // @Failure 404 {object} helpers.HTTPError 193 | // @Failure 500 {object} helpers.HTTPError 194 | // @Router /users/{id} [patch] 195 | // @Security Bearer 196 | func (uc *UserController) UpdateUser(ctx *gin.Context) { 197 | userID := ctx.Param("id") 198 | audit := uc.AuditService.InitialiseAuditLog(ctx, "list", uc.AuditCategory, userID) 199 | var user models.User 200 | var err error 201 | 202 | if err := ctx.ShouldBindJSON(&user); err != nil { 203 | uc.AuditService.CreateAudit(audit) 204 | ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 205 | return 206 | } 207 | 208 | if !slices.Contains(uc.providers, user.Provider) { 209 | uc.AuditService.CreateAudit(audit) 210 | ctx.JSON(http.StatusBadRequest, gin.H{"error": "provider does not exist", "providers": uc.providers}) 211 | return 212 | } 213 | 214 | user.ID, err = uuid.FromString(userID) 215 | if err != nil { 216 | uc.AuditService.CreateAudit(audit) 217 | ctx.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) 218 | return 219 | } 220 | 221 | user, err = uc.UserService.UpdateUser(user) 222 | if err != nil { 223 | uc.AuditService.CreateAudit(audit) 224 | ctx.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) 225 | return 226 | } 227 | audit.Status = "success" 228 | uc.AuditService.CreateAudit(audit) 229 | ctx.JSON(http.StatusOK, user) 230 | } 231 | 232 | // DeleteUser godoc 233 | // 234 | // @Summary Delete a user 235 | // @Description Delete by user ID 236 | // @Tags users 237 | // @Accept json 238 | // @Produce json 239 | // @Param id path string true "User ID" 240 | // @Success 204 {object} models.User 241 | // @Failure 400 {object} helpers.HTTPError 242 | // @Failure 404 {object} helpers.HTTPError 243 | // @Failure 500 {object} helpers.HTTPError 244 | // @Router /users/{id} [delete] 245 | // @Security Bearer 246 | func (uc *UserController) DeleteUser(ctx *gin.Context) { 247 | userID := ctx.Param("id") 248 | audit := uc.AuditService.InitialiseAuditLog(ctx, "list", uc.AuditCategory, userID) 249 | 250 | err := uc.UserService.DeleteUser(userID) 251 | if err != nil { 252 | uc.AuditService.CreateAudit(audit) 253 | ctx.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) 254 | return 255 | } 256 | audit.Status = "success" 257 | uc.AuditService.CreateAudit(audit) 258 | ctx.JSON(http.StatusOK, gin.H{"msg": "user deleted successfully"}) 259 | } 260 | -------------------------------------------------------------------------------- /controllers/webhooks.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | 8 | "github.com/kriten-io/kriten/config" 9 | "github.com/kriten-io/kriten/middlewares" 10 | "github.com/kriten-io/kriten/models" 11 | "github.com/kriten-io/kriten/services" 12 | 13 | "github.com/gin-gonic/gin" 14 | uuid "github.com/satori/go.uuid" 15 | ) 16 | 17 | type WebhookController struct { 18 | WebhookService services.WebhookService 19 | JobService services.JobService 20 | AuthService services.AuthService 21 | providers []string 22 | AuditService services.AuditService 23 | AuditCategory string 24 | } 25 | 26 | func NewWebhookController( 27 | ws services.WebhookService, 28 | js services.JobService, 29 | as services.AuthService, 30 | als services.AuditService, 31 | p []string, 32 | ) WebhookController { 33 | return WebhookController{ 34 | WebhookService: ws, 35 | JobService: js, 36 | AuthService: as, 37 | providers: p, 38 | AuditService: als, 39 | AuditCategory: "webHooks", 40 | } 41 | } 42 | 43 | func (wc *WebhookController) SetWebhookRoutes(rg *gin.RouterGroup, config config.Config) { 44 | r := rg.Group("").Use( 45 | middlewares.AuthenticationMiddleware(wc.AuthService, config.JWT)) 46 | 47 | // Authorizations is set in the svc, only returning own tokens 48 | r.GET("", wc.ListWebhooks) 49 | 50 | r.GET("/all", middlewares.SetAuthorizationListMiddleware(wc.AuthService, "webHooks"), wc.ListAllWebhooks) 51 | r.GET("/:id", middlewares.AuthorizationMiddleware(wc.AuthService, "webHooks", "read"), wc.GetWebhook) 52 | 53 | r.POST("", wc.CreateWebhook) 54 | r.PUT("", wc.CreateWebhook) 55 | 56 | r.POST("/run/:id", middlewares.AuthorizationMiddleware(wc.AuthService, "jobs", "write"), wc.RunWebhook) 57 | 58 | r.Use(middlewares.AuthorizationMiddleware(wc.AuthService, "webHooks", "write")) 59 | { 60 | //r.PATCH("/:id", wc.UpdateWebhooks) 61 | //r.PUT("/:id", wc.UpdateWebhooks) 62 | r.DELETE("/:id", wc.DeleteWebhook) 63 | } 64 | } 65 | 66 | // ListWebhooks godoc 67 | // 68 | // @Summary List own webHooks 69 | // @Description List own webHooks available on the cluster 70 | // @Tags api_tokens 71 | // @Accept json 72 | // @Produce json 73 | // @Success 200 {array} models.Webhook 74 | // @Failure 400 {object} helpers.HTTPError 75 | // @Failure 404 {object} helpers.HTTPError 76 | // @Failure 500 {object} helpers.HTTPError 77 | // @Router /webhooks [get] 78 | // @Security Bearer 79 | func (wc *WebhookController) ListWebhooks(ctx *gin.Context) { 80 | // audit := wc.AuditService.InitialiseAuditLog(ctx, "list", wc.AuditCategory, "*") 81 | userid := ctx.MustGet("userID").(uuid.UUID) 82 | webHooks, err := wc.WebhookService.ListWebhooks(userid) 83 | 84 | if err != nil { 85 | // wc.AuditService.CreateAudit(audit) 86 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 87 | return 88 | } 89 | 90 | // audit.Status = "success" 91 | ctx.Header("Content-range", fmt.Sprintf("%v", len(webHooks))) 92 | if len(webHooks) == 0 { 93 | var arr [0]int 94 | // wc.AuditService.CreateAudit(audit) 95 | ctx.JSON(http.StatusOK, arr) 96 | return 97 | } 98 | 99 | // wc.AuditService.CreateAudit(audit) 100 | ctx.SetSameSite(http.SameSiteLaxMode) 101 | ctx.JSON(http.StatusOK, webHooks) 102 | } 103 | 104 | // ListAllWebhooks godoc 105 | // 106 | // @Summary List all webHooks 107 | // @Description List all webHooks available on the cluster 108 | // @Tags webhooks 109 | // @Accept json 110 | // @Produce json 111 | // @Success 200 {array} models.Webhook 112 | // @Failure 400 {object} helpers.HTTPError 113 | // @Failure 404 {object} helpers.HTTPError 114 | // @Failure 500 {object} helpers.HTTPError 115 | // @Router /webhooks/all [get] 116 | // @Security Bearer 117 | func (wc *WebhookController) ListAllWebhooks(ctx *gin.Context) { 118 | // audit := wc.AuditService.InitialiseAuditLog(ctx, "list", wc.AuditCategory, "*") 119 | authList := ctx.MustGet("authList").([]string) 120 | webHooks, err := wc.WebhookService.ListAllWebhooks(authList) 121 | 122 | if err != nil { 123 | // wc.AuditService.CreateAudit(audit) 124 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 125 | return 126 | } 127 | 128 | // audit.Status = "success" 129 | ctx.Header("Content-range", fmt.Sprintf("%v", len(webHooks))) 130 | if len(webHooks) == 0 { 131 | var arr [0]int 132 | // wc.AuditService.CreateAudit(audit) 133 | ctx.JSON(http.StatusOK, arr) 134 | return 135 | } 136 | 137 | // wc.AuditService.CreateAudit(audit) 138 | ctx.SetSameSite(http.SameSiteLaxMode) 139 | ctx.JSON(http.StatusOK, webHooks) 140 | } 141 | 142 | // GetWebhook godoc 143 | // 144 | // @Summary Get a webHook 145 | // @Description Get information about a specific webHook 146 | // @Tags webhooks 147 | // @Accept json 148 | // @Produce json 149 | // @Param id path string true "Webhook ID" 150 | // @Success 200 {object} models.Webhook 151 | // @Failure 400 {object} helpers.HTTPError 152 | // @Failure 404 {object} helpers.HTTPError 153 | // @Failure 500 {object} helpers.HTTPError 154 | // @Router /webhooks/{id} [get] 155 | // @Security Bearer 156 | func (wc *WebhookController) GetWebhook(ctx *gin.Context) { 157 | webhookID := ctx.Param("id") 158 | // audit := wc.AuditService.InitialiseAuditLog(ctx, "get", wc.AuditCategory, webhookID) 159 | webhook, err := wc.WebhookService.GetWebhook(webhookID) 160 | 161 | if err != nil { 162 | // wc.AuditService.CreateAudit(audit) 163 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 164 | return 165 | } 166 | 167 | // audit.Status = "success" 168 | // wc.AuditService.CreateAudit(audit) 169 | ctx.JSON(http.StatusOK, webhook) 170 | } 171 | 172 | // CreateWebhook godoc 173 | // 174 | // @Summary Create a new webhook 175 | // @Description Add a webhook to the cluster 176 | // @Tags webhooks 177 | // @Accept json 178 | // @Produce json 179 | // @Param webhook body models.Webhook true "New Webhook" 180 | // @Success 200 {object} models.Webhook 181 | // @Failure 400 {object} helpers.HTTPError 182 | // @Failure 404 {object} helpers.HTTPError 183 | // @Failure 500 {object} helpers.HTTPError 184 | // @Router /webhooks [post] 185 | // @Security Bearer 186 | func (wc *WebhookController) CreateWebhook(ctx *gin.Context) { 187 | userid := ctx.MustGet("userID").(uuid.UUID) 188 | audit := wc.AuditService.InitialiseAuditLog(ctx, "create", wc.AuditCategory, "*") 189 | var webhook models.Webhook 190 | 191 | if err := ctx.ShouldBindJSON(&webhook); err != nil { 192 | wc.AuditService.CreateAudit(audit) 193 | ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 194 | return 195 | } 196 | 197 | webhook.Owner = userid 198 | 199 | webhook, err := wc.WebhookService.CreateWebhook(webhook) 200 | if err != nil { 201 | wc.AuditService.CreateAudit(audit) 202 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 203 | return 204 | } 205 | // if audit.EventTarget == "" { 206 | // audit.EventTarget = apiToken.Key 207 | // } 208 | 209 | audit.Status = "success" 210 | wc.AuditService.CreateAudit(audit) 211 | ctx.JSON(http.StatusOK, webhook) 212 | } 213 | 214 | // DeleteWebhook godoc 215 | // 216 | // @Summary Delete a webhook 217 | // @Description Delete by webhook ID 218 | // @Tags webhook 219 | // @Accept json 220 | // @Produce json 221 | // @Param id path string true "Webhook ID" 222 | // @Success 204 {object} models.Webhook 223 | // @Failure 400 {object} helpers.HTTPError 224 | // @Failure 404 {object} helpers.HTTPError 225 | // @Failure 500 {object} helpers.HTTPError 226 | // @Router /webhooks/{id} [delete] 227 | // @Security Bearer 228 | func (wc *WebhookController) DeleteWebhook(ctx *gin.Context) { 229 | webhookID := ctx.Param("id") 230 | audit := wc.AuditService.InitialiseAuditLog(ctx, "delete", wc.AuditCategory, webhookID) 231 | 232 | err := wc.WebhookService.DeleteWebhook(webhookID) 233 | if err != nil { 234 | wc.AuditService.CreateAudit(audit) 235 | ctx.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) 236 | return 237 | } 238 | audit.Status = "success" 239 | wc.AuditService.CreateAudit(audit) 240 | ctx.JSON(http.StatusOK, gin.H{"msg": "webhook deleted successfully"}) 241 | } 242 | 243 | // RunWebhook godoc 244 | // 245 | // @Summary Run webhook 246 | // @Description Execute Kriten job via webhook 247 | // @Tags webhooks 248 | // @Accept json 249 | // @Produce json 250 | // @Param id path string true "Webhook ID" 251 | // @Param evars body object false "Extra vars" 252 | // @Success 200 {object} models.Job 253 | // @Failure 400 {object} helpers.HTTPError 254 | // @Failure 404 {object} helpers.HTTPError 255 | // @Failure 500 {object} helpers.HTTPError 256 | // @Router /webhooks/run/{id} [post] 257 | // @Security Signature 258 | func (wc *WebhookController) RunWebhook(ctx *gin.Context) { 259 | webhookID := ctx.Param("id") 260 | taskID := ctx.MustGet("taskID").(string) 261 | username := ctx.MustGet("username").(string) 262 | 263 | audit := wc.AuditService.InitialiseAuditLog(ctx, "run", wc.AuditCategory, webhookID) 264 | audit.Status = "success" 265 | wc.AuditService.CreateAudit(audit) 266 | 267 | extraVars, err := io.ReadAll(ctx.Request.Body) 268 | 269 | if err != nil { 270 | wc.AuditService.CreateAudit(audit) 271 | ctx.JSON(http.StatusBadRequest, gin.H{"error": err}) 272 | return 273 | } 274 | 275 | job, err := wc.JobService.CreateJob(username, taskID, string(extraVars)) 276 | 277 | if err != nil { 278 | wc.AuditService.CreateAudit(audit) 279 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 280 | return 281 | } 282 | 283 | audit.Status = "success" 284 | 285 | if (job.ID != "") && (job.Completed != 0) { 286 | //ctx.JSON(http.StatusOK, gin.H{"id": jobID, "json_data": sync.JsonData}) 287 | wc.AuditService.CreateAudit(audit) 288 | ctx.JSON(http.StatusOK, job) 289 | return 290 | } 291 | 292 | wc.AuditService.CreateAudit(audit) 293 | ctx.JSON(http.StatusOK, gin.H{"msg": "job created successfully", "id": job.ID}) 294 | } 295 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kriten-io/kriten 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/elastic/go-elasticsearch/v8 v8.17.1 7 | github.com/gin-contrib/cors v1.7.5 8 | github.com/gin-gonic/gin v1.10.0 9 | github.com/go-errors/errors v1.5.1 10 | github.com/go-ldap/ldap/v3 v3.4.10 11 | github.com/go-openapi/loads v0.22.0 12 | github.com/go-openapi/spec v0.21.0 13 | github.com/go-openapi/strfmt v0.23.0 14 | github.com/go-openapi/validate v0.24.0 15 | github.com/golang-jwt/jwt v3.2.2+incompatible 16 | github.com/joho/godotenv v1.5.1 17 | github.com/lib/pq v1.10.9 18 | github.com/satori/go.uuid v1.2.0 19 | github.com/swaggo/files v1.0.1 20 | github.com/swaggo/gin-swagger v1.6.0 21 | github.com/swaggo/swag v1.16.4 22 | golang.org/x/crypto v0.38.0 23 | golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 24 | gorm.io/driver/postgres v1.5.11 25 | gorm.io/gorm v1.25.12 26 | k8s.io/api v0.32.3 27 | k8s.io/apimachinery v0.32.3 28 | k8s.io/client-go v0.32.3 29 | ) 30 | 31 | require ( 32 | github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect 33 | github.com/KyleBanks/depth v1.2.1 // indirect 34 | github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect 35 | github.com/bytedance/sonic v1.13.2 // indirect 36 | github.com/bytedance/sonic/loader v0.2.4 // indirect 37 | github.com/cloudwego/base64x v0.1.5 // indirect 38 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 39 | github.com/elastic/elastic-transport-go/v8 v8.6.1 // indirect 40 | github.com/emicklei/go-restful/v3 v3.12.2 // indirect 41 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 42 | github.com/gabriel-vasile/mimetype v1.4.9 // indirect 43 | github.com/gin-contrib/sse v1.1.0 // indirect 44 | github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect 45 | github.com/go-logr/logr v1.4.2 // indirect 46 | github.com/go-logr/stdr v1.2.2 // indirect 47 | github.com/go-openapi/analysis v0.23.0 // indirect 48 | github.com/go-openapi/errors v0.22.1 // indirect 49 | github.com/go-openapi/jsonpointer v0.21.1 // indirect 50 | github.com/go-openapi/jsonreference v0.21.0 // indirect 51 | github.com/go-openapi/swag v0.23.1 // indirect 52 | github.com/go-playground/locales v0.14.1 // indirect 53 | github.com/go-playground/universal-translator v0.18.1 // indirect 54 | github.com/go-playground/validator/v10 v10.26.0 // indirect 55 | github.com/goccy/go-json v0.10.5 // indirect 56 | github.com/gogo/protobuf v1.3.2 // indirect 57 | github.com/golang/protobuf v1.5.4 // indirect 58 | github.com/google/gnostic-models v0.6.9 // indirect 59 | github.com/google/go-cmp v0.7.0 // indirect 60 | github.com/google/gofuzz v1.2.0 // indirect 61 | github.com/google/uuid v1.6.0 // indirect 62 | github.com/jackc/pgpassfile v1.0.0 // indirect 63 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect 64 | github.com/jackc/pgx/v5 v5.7.4 // indirect 65 | github.com/jackc/puddle/v2 v2.2.2 // indirect 66 | github.com/jinzhu/inflection v1.0.0 // indirect 67 | github.com/jinzhu/now v1.1.5 // indirect 68 | github.com/josharian/intern v1.0.0 // indirect 69 | github.com/json-iterator/go v1.1.12 // indirect 70 | github.com/klauspost/cpuid/v2 v2.2.10 // indirect 71 | github.com/leodido/go-urn v1.4.0 // indirect 72 | github.com/mailru/easyjson v0.9.0 // indirect 73 | github.com/mattn/go-isatty v0.0.20 // indirect 74 | github.com/mitchellh/mapstructure v1.5.0 // indirect 75 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 76 | github.com/modern-go/reflect2 v1.0.2 // indirect 77 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 78 | github.com/oklog/ulid v1.3.1 // indirect 79 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect 80 | github.com/pkg/errors v0.9.1 // indirect 81 | github.com/spf13/pflag v1.0.6 // indirect 82 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 83 | github.com/ugorji/go/codec v1.2.12 // indirect 84 | github.com/x448/float16 v0.8.4 // indirect 85 | go.mongodb.org/mongo-driver v1.17.3 // indirect 86 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 87 | go.opentelemetry.io/otel v1.35.0 // indirect 88 | go.opentelemetry.io/otel/metric v1.35.0 // indirect 89 | go.opentelemetry.io/otel/trace v1.35.0 // indirect 90 | golang.org/x/arch v0.17.0 // indirect 91 | golang.org/x/net v0.40.0 // indirect 92 | golang.org/x/oauth2 v0.28.0 // indirect 93 | golang.org/x/sync v0.14.0 // indirect 94 | golang.org/x/sys v0.33.0 // indirect 95 | golang.org/x/term v0.32.0 // indirect 96 | golang.org/x/text v0.25.0 // indirect 97 | golang.org/x/time v0.11.0 // indirect 98 | golang.org/x/tools v0.31.0 // indirect 99 | google.golang.org/protobuf v1.36.6 // indirect 100 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 101 | gopkg.in/inf.v0 v0.9.1 // indirect 102 | gopkg.in/yaml.v3 v3.0.1 // indirect 103 | k8s.io/klog/v2 v2.130.1 // indirect 104 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect 105 | k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect 106 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect 107 | sigs.k8s.io/randfill v1.0.0 // indirect 108 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect 109 | sigs.k8s.io/yaml v1.4.0 // indirect 110 | ) 111 | -------------------------------------------------------------------------------- /helpers/auth.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha256" 6 | "encoding/hex" 7 | "errors" 8 | "log" 9 | "time" 10 | 11 | "github.com/kriten-io/kriten/config" 12 | "github.com/kriten-io/kriten/models" 13 | 14 | "github.com/golang-jwt/jwt" 15 | uuid "github.com/satori/go.uuid" 16 | ) 17 | 18 | func CreateJWTToken(credentials *models.Credentials, userID uuid.UUID, jwtConf config.JWTConfig) (string, error) { 19 | expirationTime := time.Now().Add(time.Second * time.Duration(jwtConf.ExpirySeconds)) 20 | 21 | claims := &models.Claims{ 22 | Username: credentials.Username, 23 | UserID: userID, 24 | Provider: credentials.Provider, 25 | StandardClaims: jwt.StandardClaims{ 26 | ExpiresAt: expirationTime.Unix(), 27 | }, 28 | } 29 | 30 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 31 | tokenString, err := token.SignedString(jwtConf.Key) 32 | if err != nil { 33 | log.Println(err) 34 | return "", err 35 | } 36 | return tokenString, nil 37 | } 38 | 39 | func ValidateJWTToken(tokenStr string, jwtConf config.JWTConfig) (*models.Claims, error) { 40 | claims := &models.Claims{} 41 | 42 | token, err := jwt.ParseWithClaims(tokenStr, claims, 43 | func(t *jwt.Token) (interface{}, error) { 44 | return jwtConf.Key, nil 45 | }) 46 | 47 | if err != nil || !token.Valid { 48 | log.Println(err) 49 | return nil, errors.New("error: invalid token") 50 | } 51 | return claims, nil 52 | } 53 | 54 | func GenerateHMAC(apiSecret string, key string) string { 55 | // Create a new HMAC by defining the hash type and the key (as byte array) 56 | h := hmac.New(sha256.New, []byte(apiSecret)) 57 | 58 | // Write Data to it 59 | h.Write([]byte(key)) 60 | 61 | // Get result and encode as hexadecimal string 62 | sha := hex.EncodeToString(h.Sum(nil)) 63 | 64 | return sha 65 | } 66 | -------------------------------------------------------------------------------- /helpers/elastic.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "log" 7 | "time" 8 | 9 | "github.com/elastic/go-elasticsearch/v8" 10 | ) 11 | 12 | type User struct { 13 | Name string `json:"name"` 14 | IP string `json:"ip"` 15 | } 16 | 17 | type Event struct { 18 | Type string `json:"type"` 19 | Category string `json:"category"` 20 | Target string `json:"target"` 21 | Status string `json:"status"` 22 | } 23 | 24 | type AuditLog struct { 25 | Timestamp time.Time `json:"@timestamp"` 26 | User User `json:"user"` 27 | Event Event `json:"event"` 28 | } 29 | 30 | type ElasticSearch struct { 31 | Enabled bool 32 | Client *elasticsearch.Client 33 | Index string 34 | } 35 | 36 | func CreateElasticSearchLog(es ElasticSearch, timestamp time.Time, user string, ip string, eventType string, category string, target string, status string) { 37 | if !es.Enabled { 38 | return 39 | } 40 | audit := AuditLogObject(timestamp, user, ip, eventType, category, target, status) 41 | 42 | data, err := json.Marshal(audit) 43 | if err != nil { 44 | log.Println("[ERROR] Error while sending audit log to ElasticSearch") 45 | log.Println(err) 46 | } 47 | 48 | res, err := es.Client.Index(es.Index, bytes.NewReader(data)) 49 | if err != nil { 50 | log.Println("[ERROR] Error while sending audit log to ElasticSearch") 51 | log.Println(err) 52 | } 53 | 54 | defer res.Body.Close() 55 | } 56 | 57 | func AuditLogObject(timestamp time.Time, user string, ip string, eventType string, category string, target string, status string) *AuditLog { 58 | return &AuditLog{ 59 | Timestamp: timestamp, 60 | User: User{ 61 | Name: user, 62 | IP: ip, 63 | }, 64 | Event: Event{ 65 | Type: eventType, 66 | Category: category, 67 | Target: target, 68 | Status: status, 69 | }, 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /helpers/httpError.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | // NewError example 6 | func NewError(ctx *gin.Context, status int, err error) { 7 | er := HTTPError{ 8 | Code: status, 9 | Message: err.Error(), 10 | } 11 | ctx.JSON(status, er) 12 | } 13 | 14 | // HTTPError example 15 | type HTTPError struct { 16 | Code int `json:"code" example:"400"` 17 | Message string `json:"message" example:"status bad request"` 18 | } 19 | -------------------------------------------------------------------------------- /helpers/ldap.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strings" 7 | 8 | "github.com/kriten-io/kriten/config" 9 | 10 | "github.com/go-errors/errors" 11 | "github.com/go-ldap/ldap/v3" 12 | ) 13 | 14 | const Filter = "(&(objectClass=organizationalPerson)(sAMAccountName=%s))" 15 | 16 | // Ldap Connection without TLS 17 | func ConnectLDAP(config config.LDAPConfig) (*ldap.Conn, error) { 18 | l, err := ldap.DialURL(fmt.Sprintf("ldap://%s:%d", config.FQDN, config.Port)) 19 | if err != nil { 20 | log.Println("LDAP connection error: ", err) 21 | return nil, err 22 | } 23 | 24 | err = l.Bind(config.BindUser, config.BindPass) 25 | 26 | if err != nil { 27 | log.Println("Error during readonly Bind", err) 28 | return nil, err 29 | } 30 | 31 | return l, nil 32 | } 33 | 34 | // Normal Bind and Search 35 | func BindAndSearch(config config.LDAPConfig, user string, password string) error { 36 | l, err := ConnectLDAP(config) 37 | 38 | if err != nil { 39 | return err 40 | } 41 | defer l.Close() 42 | 43 | searchReq := ldap.NewSearchRequest( 44 | config.BaseDN, 45 | ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, 46 | fmt.Sprintf(Filter, user), 47 | []string{}, 48 | nil, 49 | ) 50 | 51 | result, err := l.Search(searchReq) 52 | if err != nil { 53 | return fmt.Errorf("search Error: %s", err) 54 | } 55 | 56 | if len(result.Entries) == 0 { 57 | return errors.New("User doesn't exist") 58 | } 59 | 60 | userdn := result.Entries[0].DN 61 | 62 | // Bind as the user to verify their password 63 | err = l.Bind(userdn, password) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | return nil 69 | } 70 | 71 | // Query user's groups 72 | func GetADGroups(config config.LDAPConfig, user string) ([]string, error) { 73 | var groups []string 74 | 75 | l, err := ConnectLDAP(config) 76 | 77 | if err != nil { 78 | log.Println(err) 79 | return nil, err 80 | } 81 | defer l.Close() 82 | 83 | searchReq := ldap.NewSearchRequest( 84 | config.BaseDN, 85 | ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, 86 | fmt.Sprintf(Filter, user), 87 | []string{}, 88 | nil, 89 | ) 90 | 91 | result, err := l.Search(searchReq) 92 | if err != nil { 93 | log.Println(err) 94 | return nil, err 95 | } 96 | 97 | if len(result.Entries) == 0 { 98 | return nil, errors.New("User " + user + " doesn't exist") 99 | } 100 | 101 | entries := result.Entries[0].GetAttributeValues("memberOf") 102 | 103 | for _, entry := range entries { 104 | s := strings.Split(entry, "CN=")[1] 105 | s = strings.Split(strings.ToLower(s), ",")[0] 106 | groups = append(groups, s) 107 | } 108 | 109 | log.Println("user is member of: ", groups) 110 | 111 | return groups, nil 112 | } 113 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "path/filepath" 7 | "time" 8 | 9 | "github.com/kriten-io/kriten/config" 10 | "github.com/kriten-io/kriten/controllers" 11 | "github.com/kriten-io/kriten/services" 12 | 13 | docs "github.com/kriten-io/kriten/docs" 14 | 15 | swaggerfiles "github.com/swaggo/files" 16 | ginSwagger "github.com/swaggo/gin-swagger" 17 | "gorm.io/driver/postgres" 18 | "gorm.io/gorm" 19 | "gorm.io/gorm/logger" 20 | 21 | "github.com/gin-contrib/cors" 22 | "github.com/gin-gonic/gin" 23 | "github.com/joho/godotenv" 24 | "k8s.io/client-go/kubernetes" 25 | "k8s.io/client-go/rest" 26 | "k8s.io/client-go/tools/clientcmd" 27 | "k8s.io/client-go/util/homedir" 28 | ) 29 | 30 | var ( 31 | router *gin.Engine 32 | as services.AuthService 33 | rs services.RunnerService 34 | ts services.TaskService 35 | js services.JobService 36 | cjs services.CronJobService 37 | us services.UserService 38 | ats services.ApiTokenService 39 | ws services.WebhookService 40 | gs services.GroupService 41 | als services.AuditService 42 | rls services.RoleService 43 | rbs services.RoleBindingService 44 | ac controllers.AuthController 45 | alc controllers.AuditController 46 | rc controllers.RunnerController 47 | tc controllers.TaskController 48 | jc controllers.JobController 49 | cjc controllers.CronJobController 50 | uc controllers.UserController 51 | atc controllers.ApiTokenController 52 | wc controllers.WebhookController 53 | gc controllers.GroupController 54 | rlc controllers.RoleController 55 | rbc controllers.RoleBindingController 56 | conf config.Config 57 | kubeConfig *rest.Config 58 | // es helpers.ElasticSearch 59 | db *gorm.DB 60 | 61 | GitBranch string 62 | ) 63 | 64 | var authProviders = []string{"local", "active_directory"} 65 | 66 | func init() { 67 | // Loading env variables and creating the config 68 | err := godotenv.Load(".env") 69 | if err != nil { 70 | log.Fatal("Error loading .env file") 71 | } 72 | conf = config.NewConfig(GitBranch) 73 | 74 | // Retrieving k8s clientset 75 | if conf.Environment == "production" { 76 | // creates the in-cluster config 77 | kubeConfig, err = rest.InClusterConfig() 78 | if err != nil { 79 | panic(err.Error()) 80 | } 81 | } else { 82 | // Kubeconfig file will be fetched from the home folder for development purpose. 83 | home := homedir.HomeDir() 84 | configPath := filepath.Join(home, ".kube", "config") 85 | log.Printf("Using local kube config path: %s\n", configPath) 86 | kubeConfig, err = clientcmd.BuildConfigFromFlags("", configPath) 87 | if err != nil { 88 | panic(err.Error()) 89 | } 90 | } 91 | conf.Kube.Clientset, err = kubernetes.NewForConfig(kubeConfig) 92 | if err != nil { 93 | panic(err.Error()) 94 | } 95 | 96 | // Establishing connection with PostgreSQL database 97 | dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%v sslmode=%s", 98 | conf.DB.Host, 99 | conf.DB.User, 100 | conf.DB.Password, 101 | conf.DB.Name, 102 | conf.DB.Port, 103 | conf.DB.SSL, 104 | ) 105 | 106 | connected := false 107 | for !connected { 108 | db, err = gorm.Open(postgres.New(postgres.Config{ 109 | DSN: dsn, 110 | PreferSimpleProtocol: true, // disables implicit prepared statement usage 111 | }), &gorm.Config{ 112 | Logger: logger.Default.LogMode(logger.Silent), 113 | }) 114 | if err != nil { 115 | log.Println("Error while connecting to Postgres") 116 | log.Println(err) 117 | log.Println("Retrying in 30 seconds..") 118 | time.Sleep(30 * time.Second) 119 | } else { 120 | connected = true 121 | } 122 | } 123 | config.InitDB(db) 124 | 125 | // if conf.ElasticSearch.CloudID != "" { 126 | // es.Client, err = elasticsearch.NewClient( 127 | // elasticsearch.Config{ 128 | // CloudID: conf.ElasticSearch.CloudID, 129 | // APIKey: conf.ElasticSearch.APIKey, 130 | // }) 131 | // es.Index = conf.ElasticSearch.Index 132 | // 133 | // if err != nil { 134 | // log.Println("Error while connecting to ElasticSearch") 135 | // log.Println(err) 136 | // } else { 137 | // es.Enabled = true 138 | // } 139 | // } 140 | } 141 | 142 | func init() { 143 | // Services 144 | us = services.NewUserService(db, conf) 145 | ats = services.NewApiTokenService(db, conf) 146 | ws = services.NewWebhookService(db, conf) 147 | gs = services.NewGroupService(db, us, conf) 148 | rls = services.NewRoleService(db, conf, &rbs, &us) 149 | rbs = services.NewRoleBindingService(db, conf, rls, gs) 150 | as = services.NewAuthService(conf, us, rls, rbs, db) 151 | als = services.NewAuditService(db, conf) 152 | 153 | rs = services.NewRunnerService(conf) 154 | ts = services.NewTaskService(ws, conf) 155 | js = services.NewJobService(conf) 156 | cjs = services.NewCronJobService(conf) 157 | 158 | // Controllers 159 | uc = controllers.NewUserController(us, gs, as, als, authProviders) 160 | wc = controllers.NewWebhookController(ws, js, as, als, authProviders) 161 | atc = controllers.NewApiTokenController(ats, as, als, authProviders) 162 | gc = controllers.NewGroupController(gs, as, als, authProviders) 163 | rlc = controllers.NewRoleController(rls, as, als) 164 | rbc = controllers.NewRoleBindingController(rbs, as, als, authProviders) 165 | ac = controllers.NewAuthController(as, als, authProviders) 166 | alc = controllers.NewAuditController(als, as) 167 | 168 | rc = controllers.NewRunnerController(rs, as, als) 169 | tc = controllers.NewTaskController(ts, as, als) 170 | jc = controllers.NewJobController(js, as, als) 171 | cjc = controllers.NewCronJobController(cjs, as, als) 172 | } 173 | 174 | // @title Swagger Kriten 175 | // @version v0.3 176 | // @description API Gateway for your kubernetes services. 177 | // @termsOfService http://swagger.io/terms/ 178 | 179 | // @contact.name Evolvere Support 180 | // @contact.url https://www.evolvere-tech.co.uk/contact 181 | // @contact.email info@evolvere-tech.co.uk 182 | 183 | // @license.name Apache 2.0 184 | // @license.url http://www.apache.org/licenses/LICENSE-2.0.html 185 | 186 | // @BasePath /api/v1 187 | 188 | // @securityDefinitions.apikey Bearer 189 | // @in header 190 | // @name Authorization 191 | // @description Type "Bearer" followed by a space and JWT token. 192 | 193 | func main() { 194 | // API endpoints definition, fields starting with ':' are not fixed and can contain any string 195 | // Expected path: /api/v1/runner/:rname/task/:tname 196 | router = gin.Default() 197 | router.Use(cors.New(cors.Config{ 198 | AllowOrigins: []string{"*"}, 199 | AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"}, 200 | AllowHeaders: []string{"Origin", "Content-Type", "Content-Range", "Authorization"}, 201 | ExposeHeaders: []string{"Content-Length"}, 202 | AllowCredentials: true, 203 | })) 204 | 205 | router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler)) 206 | 207 | docs.SwaggerInfo.BasePath = "/api/v1" 208 | basepath := router.Group("/api/v1") 209 | { 210 | ac.SetAuthRoutes(basepath) 211 | audit := basepath.Group("/audit_logs") 212 | runners := basepath.Group("/runners") 213 | tasks := basepath.Group("/tasks") 214 | jobs := basepath.Group("/jobs") 215 | cronjobs := basepath.Group("/cronjobs") 216 | users := basepath.Group("/users") 217 | tokens := basepath.Group("/api_tokens") 218 | groups := basepath.Group("/groups") 219 | roles := basepath.Group("/roles") 220 | roleBindings := basepath.Group("/role_bindings") 221 | webhooks := basepath.Group("/webhooks") 222 | { 223 | alc.SetAuditRoutes(audit, conf) 224 | rc.SetRunnerRoutes(runners, conf) 225 | tc.SetTaskRoutes(tasks, conf) 226 | jc.SetJobRoutes(jobs, conf) 227 | cjc.SetCronJobRoutes(cronjobs, conf) 228 | uc.SetUserRoutes(users, conf) 229 | atc.SetApiTokenRoutes(tokens, conf) 230 | wc.SetWebhookRoutes(webhooks, conf) 231 | gc.SetGroupRoutes(groups, conf) 232 | rlc.SetRoleRoutes(roles, conf) 233 | rbc.SetRoleBindingRoutes(roleBindings, conf) 234 | } 235 | } 236 | 237 | log.Fatal(router.Run()) 238 | } 239 | -------------------------------------------------------------------------------- /middlewares/auth.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "log" 7 | "net/http" 8 | "strings" 9 | 10 | "github.com/kriten-io/kriten/config" 11 | "github.com/kriten-io/kriten/helpers" 12 | "github.com/kriten-io/kriten/models" 13 | "github.com/kriten-io/kriten/services" 14 | 15 | "github.com/gin-gonic/gin" 16 | uuid "github.com/satori/go.uuid" 17 | ) 18 | 19 | func AuthenticationMiddleware(as services.AuthService, jwtConf config.JWTConfig) gin.HandlerFunc { 20 | return func(ctx *gin.Context) { 21 | var token string 22 | webhookID := ctx.Param("id") 23 | // webhook-timestamp, webhook-id and webhook-signature - are specific header fields of Opsmil Infrahub webhook 24 | webhookTimestamp := ctx.GetHeader("webhook-timestamp") 25 | webhookMsgID := ctx.GetHeader("webhook-id") 26 | webhookSig := ctx.GetHeader("webhook-signature") 27 | // X-Hook-Signature header field is common webhook signature field, supported by Netbox and Nautobot 28 | signature := ctx.GetHeader("X-Hook-Signature") 29 | token = ctx.GetHeader("Token") 30 | if strings.Contains(ctx.Request.URL.String(), "/api/v1/webhooks/run") { 31 | if signature == "" && webhookSig == "" { 32 | ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "webhook authentication failed."}) 33 | return 34 | } 35 | 36 | body, err := io.ReadAll(ctx.Request.Body) 37 | 38 | if err != nil { 39 | ctx.JSON(http.StatusBadRequest, gin.H{"error": "failed to read request body"}) 40 | return 41 | } 42 | 43 | if webhookMsgID != "" && webhookTimestamp != "" && webhookSig != "" { 44 | owner, taskID, err := as.ValidateWebhookSignatureInfraHub( 45 | webhookID, 46 | webhookMsgID, 47 | webhookTimestamp, 48 | webhookSig, 49 | body) 50 | if err != nil { 51 | ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "webhook authentication failed."}) 52 | return 53 | } 54 | ctx.Set("userID", owner.ID) 55 | ctx.Set("username", owner.Username) 56 | ctx.Set("provider", owner.Provider) 57 | ctx.Set("taskID", taskID) 58 | } else if signature != "" { 59 | owner, taskID, err := as.ValidateWebhookSignatureCommon( 60 | webhookID, 61 | signature, 62 | body) 63 | if err != nil { 64 | ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "webhook authentication failed."}) 65 | return 66 | } 67 | ctx.Set("userID", owner.ID) 68 | ctx.Set("username", owner.Username) 69 | ctx.Set("provider", owner.Provider) 70 | ctx.Set("taskID", taskID) 71 | } else { 72 | ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "webhook authentication failed."}) 73 | return 74 | } 75 | 76 | ctx.Request.Body = io.NopCloser(bytes.NewBuffer(body)) 77 | } else if token != "" { 78 | owner, err := as.ValidateAPIToken(token) 79 | if err != nil { 80 | ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) 81 | return 82 | } 83 | ctx.Set("userID", owner.ID) 84 | ctx.Set("username", owner.Username) 85 | ctx.Set("provider", owner.Provider) 86 | } else { 87 | // If no API token is provided, checking for Bearer or Cookies 88 | bearer := strings.Split(ctx.GetHeader("Authorization"), "Bearer ") 89 | if len(bearer) > 1 { 90 | token = bearer[1] 91 | } 92 | cookie, err := ctx.Request.Cookie("token") 93 | if token == "" { 94 | if err != nil { 95 | ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "please authenticate."}) 96 | return 97 | } 98 | token = cookie.Value 99 | } 100 | 101 | claims, err := helpers.ValidateJWTToken(token, jwtConf) 102 | if err != nil { 103 | ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token."}) 104 | return 105 | } 106 | 107 | ctx.Set("userID", claims.UserID) 108 | ctx.Set("username", claims.Username) 109 | ctx.Set("provider", claims.Provider) 110 | } 111 | ctx.Next() 112 | } 113 | } 114 | 115 | func AuthorizationMiddleware(as services.AuthService, resource string, access string) gin.HandlerFunc { 116 | return func(ctx *gin.Context) { 117 | userID := ctx.MustGet("userID").(uuid.UUID) 118 | provider := ctx.MustGet("provider").(string) 119 | requestUrl := ctx.Request.URL.String() 120 | 121 | resourceID := ctx.Param("id") 122 | if resourceID == "" { 123 | resourceID = "*" 124 | } 125 | 126 | // trimming last 6 chars for jobs read because 127 | // jobs include random caracters at the end 128 | if resource == "jobs" && access == "read" && !strings.HasSuffix(requestUrl, "/schema") { 129 | resourceID = resourceID[:len(resourceID)-6] 130 | } 131 | 132 | isAuthorised, err := as.IsAutorised( 133 | &models.Authorization{ 134 | UserID: userID, 135 | Provider: provider, 136 | Resource: resource, 137 | ResourceID: resourceID, 138 | Access: access, 139 | }, 140 | ) 141 | if err != nil { 142 | log.Println(err) 143 | ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "internal server error."}) 144 | return 145 | } 146 | 147 | if isAuthorised { 148 | ctx.Next() 149 | return 150 | } 151 | 152 | ctx.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "unauthorized - user cannot access resource"}) 153 | } 154 | } 155 | 156 | func SetAuthorizationListMiddleware(as services.AuthService, resource string) gin.HandlerFunc { 157 | return func(ctx *gin.Context) { 158 | userID := ctx.MustGet("userID").(uuid.UUID) 159 | provider := ctx.MustGet("provider").(string) 160 | 161 | authList, err := as.GetAuthorizationList( 162 | &models.Authorization{ 163 | UserID: userID, 164 | Provider: provider, 165 | Resource: resource, 166 | }, 167 | ) 168 | if err != nil { 169 | log.Println(err) 170 | ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "internal server error."}) 171 | return 172 | } 173 | 174 | ctx.Set("authList", authList) 175 | ctx.Next() 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /models/audit.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | uuid "github.com/satori/go.uuid" 7 | ) 8 | 9 | type AuditLog struct { 10 | ID uuid.UUID `gorm:"column:auditlog_id;type:uuid;default:gen_random_uuid()" json:"id"` 11 | // Timestamp time.Time `json:"@timestamp"` 12 | UserID uuid.UUID `gorm:"column:user_id" json:"user_id"` 13 | UserName string `gorm:"column:user_name" json:"username"` 14 | Provider string `gorm:"column:provider" json:"provider"` 15 | EventType string `gorm:"column:event_type" json:"event_type"` 16 | EventCategory string `gorm:"column:event_category" json:"event_category"` 17 | EventTarget string `gorm:"column:event_target" json:"event_target"` 18 | Status string `gorm:"column:status" json:"status"` 19 | CreatedAt time.Time `json:"created_at"` 20 | UpdatedAt time.Time `json:"updated_at"` 21 | } 22 | -------------------------------------------------------------------------------- /models/credentials.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/golang-jwt/jwt" 5 | uuid "github.com/satori/go.uuid" 6 | ) 7 | 8 | type Credentials struct { 9 | Username string `json:"username" binding:"required"` 10 | Password string `json:"password" binding:"required"` 11 | Provider string `json:"provider" binding:"required"` 12 | } 13 | 14 | type Claims struct { 15 | Username string `json:"username"` 16 | UserID uuid.UUID `json:"user_id"` 17 | Provider string `json:"provider"` 18 | jwt.StandardClaims 19 | } 20 | 21 | type Authorization struct { 22 | Username string `json:"username"` 23 | UserID uuid.UUID `json:"user_id"` 24 | Provider string `json:"provider"` 25 | Resource string `json:"resource"` 26 | ResourceID string `json:"resource_id"` 27 | Access string `json:"access"` 28 | } 29 | -------------------------------------------------------------------------------- /models/cronjob.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type CronJob struct { 4 | Name string `json:"name"` 5 | Owner string `json:"owner"` 6 | Task string `json:"task"` 7 | Schedule string `json:"schedule"` 8 | Disable bool `json:"disable"` 9 | ExtraVars map[string]interface{} `json:"extra_vars,omitempty"` 10 | } 11 | -------------------------------------------------------------------------------- /models/group.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/lib/pq" 7 | uuid "github.com/satori/go.uuid" 8 | ) 9 | 10 | type Group struct { 11 | Name string `gorm:"uniqueIndex:idx_group,priority:2;<-:create" json:"name" binding:"required"` 12 | Provider string `gorm:"uniqueIndex:idx_group,priority:1" json:"provider" binding:"required"` 13 | Users pq.StringArray `gorm:"column:users;type:text[]" json:"users"` 14 | Builtin bool `json:"-"` 15 | CreatedAt time.Time `json:"created_at"` 16 | UpdatedAt time.Time `json:"updated_at"` 17 | ID uuid.UUID `gorm:"column:group_id;type:uuid;default:gen_random_uuid()" json:"id"` 18 | } 19 | 20 | type GroupUser struct { 21 | Username string `json:"name"` 22 | Provider string `json:"provider"` 23 | ID uuid.UUID `json:"id,omitempty"` 24 | } 25 | -------------------------------------------------------------------------------- /models/job.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Job struct { 4 | ID string `json:"id"` 5 | Owner string `json:"owner"` 6 | StartTime string `json:"start_time,omitempty"` 7 | CompletionTime string `json:"completion_time,omitempty"` 8 | Failed int32 `json:"failed"` 9 | Completed int32 `json:"completed"` 10 | Stdout string `json:"stdout"` 11 | JsonData map[string]interface{} `json:"json_data"` 12 | } 13 | -------------------------------------------------------------------------------- /models/role.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/lib/pq" 7 | uuid "github.com/satori/go.uuid" 8 | ) 9 | 10 | type Role struct { 11 | ID uuid.UUID `gorm:"column:role_id;type:uuid;default:gen_random_uuid()" json:"id"` 12 | Name string `gorm:"uniqueIndex;<-:create" json:"name" binding:"required"` 13 | Resource string `json:"resource" binding:"required"` 14 | Resource_IDs pq.StringArray `gorm:"type:text[]" json:"resource_ids" binding:"required,unique"` 15 | Access string `json:"access" binding:"required"` 16 | Builtin bool `json:"-"` 17 | CreatedAt time.Time `json:"created_at"` 18 | UpdatedAt time.Time `json:"updated_at"` 19 | } 20 | -------------------------------------------------------------------------------- /models/role_binding.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | uuid "github.com/satori/go.uuid" 7 | ) 8 | 9 | type RoleBinding struct { 10 | ID uuid.UUID `gorm:"column:role_binding_id;type:uuid;default:gen_random_uuid()" json:"id"` 11 | Name string `gorm:"uniqueIndex;<-:create" json:"name" binding:"required"` 12 | RoleID uuid.UUID `gorm:"column:role_id;type:uuid" json:"role_id"` 13 | RoleName string `gorm:"column:role_name" json:"role_name" binding:"required"` 14 | SubjectKind string `json:"subject_kind" binding:"required"` 15 | SubjectProvider string `gorm:"index" json:"subject_provider" binding:"required"` 16 | SubjectID uuid.UUID `json:"subject_id"` 17 | SubjectName string `gorm:"column:subject_name" json:"subject_name" binding:"required"` 18 | Builtin bool `json:"-"` 19 | CreatedAt time.Time `json:"created_at"` 20 | UpdatedAt time.Time `json:"updated_at"` 21 | } 22 | -------------------------------------------------------------------------------- /models/runner.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Runner struct { 4 | Secret map[string]string `json:"secret,omitempty"` 5 | Name string `json:"name" binding:"required"` 6 | Image string `json:"image" binding:"required"` 7 | GitURL string `json:"gitURL" binding:"required"` 8 | Token string `json:"token"` 9 | Branch string `json:"branch"` 10 | } 11 | -------------------------------------------------------------------------------- /models/task.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Task struct { 4 | Schema map[string]any `json:"schema,omitempty"` 5 | Name string `json:"name" binding:"required"` 6 | Runner string `json:"runner" binding:"required"` 7 | Command string `json:"command" binding:"required"` 8 | Synchronous bool `json:"synchronous"` 9 | } 10 | -------------------------------------------------------------------------------- /models/token.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | uuid "github.com/satori/go.uuid" 7 | ) 8 | 9 | type ApiToken struct { 10 | ID uuid.UUID `gorm:"column:id;type:uuid;default:gen_random_uuid()" json:"id"` 11 | Owner uuid.UUID `gorm:"type:uuid" json:"owner"` 12 | Enabled *bool `json:"enabled"` 13 | Expires *time.Time `json:"expires"` 14 | Key string `json:"key,omitempty"` 15 | Description string `json:"description,omitempty"` 16 | CreatedAt time.Time `json:"created_at"` 17 | UpdatedAt time.Time `json:"updated_at"` 18 | } 19 | -------------------------------------------------------------------------------- /models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/lib/pq" 7 | uuid "github.com/satori/go.uuid" 8 | ) 9 | 10 | type User struct { 11 | ID uuid.UUID `gorm:"column:user_id;type:uuid;default:gen_random_uuid()" json:"id"` 12 | Username string `gorm:"uniqueIndex:idx_user,priority:2;<-:create" json:"name" binding:"required"` 13 | Password string `json:"password" binding:"required"` 14 | Provider string `gorm:"uniqueIndex:idx_user,priority:1" json:"provider" binding:"required"` 15 | Groups pq.StringArray `gorm:"column:groups;type:text[]" json:"groups"` 16 | Builtin bool `json:"-"` 17 | CreatedAt time.Time `json:"created_at"` 18 | UpdatedAt time.Time `json:"updated_at"` 19 | } 20 | 21 | type UserGroup struct { 22 | Name string `json:"name"` 23 | Provider string `json:"provider"` 24 | ID uuid.UUID `json:"id"` 25 | } 26 | -------------------------------------------------------------------------------- /models/webhook.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | uuid "github.com/satori/go.uuid" 7 | ) 8 | 9 | type Webhook struct { 10 | ID uuid.UUID `gorm:"column:id;type:uuid;default:gen_random_uuid()" json:"id"` 11 | Owner uuid.UUID `gorm:"type:uuid" json:"owner"` 12 | Secret string `json:"secret,omitempty"` 13 | Description string `json:"description,omitempty"` 14 | Task string `json:"task,omitempty"` 15 | CreatedAt time.Time `json:"created_at"` 16 | UpdatedAt time.Time `json:"updated_at"` 17 | } 18 | -------------------------------------------------------------------------------- /services/audit_svc.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/kriten-io/kriten/config" 8 | "github.com/kriten-io/kriten/models" 9 | 10 | "github.com/gin-gonic/gin" 11 | uuid "github.com/satori/go.uuid" 12 | "gorm.io/gorm" 13 | ) 14 | 15 | type AuditService interface { 16 | ListAuditLogs(int) ([]models.AuditLog, error) 17 | GetAuditLog(string) (models.AuditLog, error) 18 | CreateAudit(models.AuditLog) 19 | InitialiseAuditLog(*gin.Context, string, string, string) models.AuditLog 20 | } 21 | 22 | type AuditServiceImpl struct { 23 | db *gorm.DB 24 | config config.Config 25 | } 26 | 27 | func NewAuditService(database *gorm.DB, config config.Config) AuditService { 28 | return &AuditServiceImpl{ 29 | db: database, 30 | config: config, 31 | } 32 | } 33 | 34 | func (a *AuditServiceImpl) ListAuditLogs(num int) ([]models.AuditLog, error) { 35 | var logs []models.AuditLog 36 | res := a.db.Order("created_at desc").Limit(num).Find(&logs) 37 | if res.Error != nil { 38 | return logs, res.Error 39 | } 40 | 41 | return logs, nil 42 | } 43 | 44 | func (a *AuditServiceImpl) GetAuditLog(id string) (models.AuditLog, error) { 45 | var log models.AuditLog 46 | res := a.db.Where("auditlog_id = ?", id).Find(&log) 47 | if res.Error != nil { 48 | return models.AuditLog{}, res.Error 49 | } 50 | 51 | if log.UserName == "" { 52 | return models.AuditLog{}, fmt.Errorf("audit log %s not found, please check uuid", id) 53 | } 54 | 55 | return log, nil 56 | } 57 | 58 | func (a *AuditServiceImpl) CreateAudit(auditlog models.AuditLog) { 59 | res := a.db.Create(&auditlog) 60 | if res.Error != nil { 61 | log.Println("Error during Audit creation: " + res.Error.Error()) 62 | } 63 | } 64 | 65 | func (a *AuditServiceImpl) InitialiseAuditLog( 66 | ctx *gin.Context, 67 | eventType string, 68 | category string, 69 | target string, 70 | ) models.AuditLog { 71 | var userID uuid.UUID 72 | var username, provider string 73 | uid, _ := ctx.Get("userID") 74 | if uid != nil { 75 | userID = uid.(uuid.UUID) 76 | uname, _ := ctx.Get("username") 77 | prov, _ := ctx.Get("provider") 78 | 79 | username = uname.(string) 80 | provider = prov.(string) 81 | } 82 | 83 | return models.AuditLog{ 84 | UserID: userID, 85 | UserName: username, 86 | Provider: provider, 87 | EventType: eventType, 88 | EventCategory: category, 89 | EventTarget: target, 90 | Status: "error", // status will be updated later if successful 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /services/auth_svc.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha256" 6 | "crypto/sha512" 7 | "encoding/base64" 8 | "encoding/hex" 9 | "errors" 10 | "fmt" 11 | "log" 12 | "strings" 13 | "time" 14 | 15 | "github.com/kriten-io/kriten/config" 16 | "github.com/kriten-io/kriten/helpers" 17 | "github.com/kriten-io/kriten/models" 18 | 19 | "github.com/golang-jwt/jwt" 20 | "golang.org/x/crypto/bcrypt" 21 | "golang.org/x/exp/slices" 22 | "gorm.io/gorm" 23 | ) 24 | 25 | type AuthService interface { 26 | Login(*models.Credentials) (string, int, error) 27 | Refresh(string) (string, int, error) 28 | IsAutorised(*models.Authorization) (bool, error) 29 | GetAuthorizationList(*models.Authorization) ([]string, error) 30 | ValidateAPIToken(string) (models.User, error) 31 | ValidateWebhookSignatureInfraHub(string, string, string, string, []byte) (models.User, string, error) 32 | ValidateWebhookSignatureCommon(string, string, []byte) (models.User, string, error) 33 | } 34 | 35 | type AuthServiceImpl struct { 36 | db *gorm.DB 37 | UserService UserService 38 | RoleService RoleService 39 | RoleBindingService RoleBindingService 40 | config config.Config 41 | } 42 | 43 | func NewAuthService( 44 | config config.Config, 45 | us UserService, 46 | rls RoleService, 47 | rbc RoleBindingService, 48 | database *gorm.DB, 49 | ) AuthService { 50 | return &AuthServiceImpl{ 51 | config: config, 52 | UserService: us, 53 | RoleService: rls, 54 | RoleBindingService: rbc, 55 | db: database, 56 | } 57 | } 58 | 59 | // Login - TODO: This function is getting very crowded 60 | // might need to be refactored in the future. 61 | func (a *AuthServiceImpl) Login(credentials *models.Credentials) (string, int, error) { 62 | var user models.User 63 | var err error 64 | 65 | if credentials.Username == "root" { 66 | rootPassword, err := a.GetRootPassword() 67 | if err != nil { 68 | return "", -1, fmt.Errorf("failed to get root password: %w", err) 69 | } 70 | if credentials.Password != rootPassword { 71 | err := errors.New("password is incorrect") 72 | return "", -1, fmt.Errorf("failed to authenticate: %w", err) 73 | } 74 | user, err = a.UserService.GetByUsernameAndProvider(credentials.Username, credentials.Provider) 75 | if err != nil { 76 | return "", -1, fmt.Errorf("user not found: %w", err) 77 | } 78 | } else if credentials.Provider == "local" { 79 | user, err = a.UserService.GetByUsernameAndProvider(credentials.Username, credentials.Provider) 80 | if err != nil { 81 | return "", -1, fmt.Errorf("user not found: %w", err) 82 | } 83 | err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(credentials.Password)) 84 | if err != nil { 85 | return "", -1, fmt.Errorf("incorrect password: %w", err) 86 | } 87 | } else if credentials.Provider == "active_directory" { 88 | err := helpers.BindAndSearch(a.config.LDAP, credentials.Username, credentials.Password) 89 | if err != nil { 90 | return "", -1, fmt.Errorf("failed to authenticate: %w", err) 91 | } 92 | _, err = a.UserService.CreateUser(models.User{ 93 | Username: credentials.Username, 94 | Provider: credentials.Provider, 95 | }) 96 | if err != nil && !strings.Contains(err.Error(), "ERROR: duplicate key value violates unique constraint") { 97 | log.Println(err.Error()) 98 | return "", -1, fmt.Errorf("failed to create ldap user into local user db: %w", err) 99 | } 100 | user, err = a.UserService.GetByUsernameAndProvider(credentials.Username, credentials.Provider) 101 | if err != nil { 102 | return "", -1, fmt.Errorf("failed to get user credentials: %w", err) 103 | } 104 | } else { 105 | err := errors.New("provider does not exist") 106 | return "", -1, fmt.Errorf("unknown provider: %w", err) 107 | } 108 | 109 | token, err := helpers.CreateJWTToken(credentials, user.ID, a.config.JWT) 110 | if err != nil { 111 | log.Println(err) 112 | return "", -1, fmt.Errorf("failed to create token: %w", err) 113 | } 114 | 115 | return token, a.config.JWT.ExpirySeconds, nil 116 | } 117 | 118 | func (a *AuthServiceImpl) Refresh(tokenStr string) (string, int, error) { 119 | claims, err := helpers.ValidateJWTToken(tokenStr, a.config.JWT) 120 | if err != nil { 121 | return "", -1, fmt.Errorf("failed to validate token: %w", err) 122 | } 123 | 124 | expirationTime := time.Now().Add(time.Second * time.Duration(a.config.JWT.ExpirySeconds)) 125 | 126 | claims.ExpiresAt = expirationTime.Unix() 127 | 128 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 129 | tokenStr, err = token.SignedString(a.config.JWT.Key) 130 | if err != nil { 131 | log.Println(err) 132 | return "", -1, fmt.Errorf("failed to refresh token: %w", err) 133 | } 134 | 135 | return tokenStr, a.config.JWT.ExpirySeconds, nil 136 | } 137 | 138 | func (a *AuthServiceImpl) GetRootPassword() (string, error) { 139 | secret, err := helpers.GetSecret(a.config.Kube, a.config.RootSecret) 140 | 141 | if err != nil { 142 | return "", fmt.Errorf("failed to get secret for root user: %w", err) 143 | } 144 | 145 | password := secret.Data["password"] 146 | 147 | return string(password), nil 148 | } 149 | 150 | func (a *AuthServiceImpl) ValidateAPIToken(key string) (models.User, error) { 151 | var apiToken models.ApiToken 152 | apiKey := helpers.GenerateHMAC(a.config.APISecret, key) 153 | 154 | res := a.db.Where("key = ?", apiKey).Find(&apiToken) 155 | if res.Error != nil { 156 | return models.User{}, res.Error 157 | } 158 | 159 | // checking if there's any result 160 | if res.RowsAffected == 0 { 161 | return models.User{}, errors.New("invalid token") 162 | } 163 | 164 | if !apiToken.Expires.IsZero() && apiToken.Expires.Before(time.Now()) { 165 | return models.User{}, errors.New("token expired") 166 | } 167 | if !*apiToken.Enabled { 168 | return models.User{}, errors.New("token not enabled") 169 | } 170 | 171 | // Token is Valid, retrieving User info 172 | var user models.User 173 | res = a.db.Where("user_id = ?", apiToken.Owner).Find(&user) 174 | if res.Error != nil { 175 | return models.User{}, res.Error 176 | } 177 | return user, nil 178 | } 179 | 180 | func (a *AuthServiceImpl) ValidateWebhookSignatureInfraHub( 181 | id string, 182 | msgID string, 183 | msgTimestamp string, 184 | signature string, 185 | body []byte, 186 | ) (models.User, string, error) { 187 | // Splitting the signature to get the to remove prepended "v1," from InfraHub 188 | split := strings.Split(signature, ",") 189 | if len(split) != 2 { 190 | return models.User{}, "", errors.New("invalid signature") 191 | } 192 | signature = split[1] 193 | data := []byte(fmt.Sprintf("%s.%s.", msgID, msgTimestamp)) 194 | data = append(data, body...) 195 | 196 | var webhook models.Webhook 197 | res := a.db.Where("id = ?", id).Find(&webhook) 198 | if res.Error != nil { 199 | return models.User{}, "", res.Error 200 | } 201 | // checking if there's any result 202 | if res.RowsAffected == 0 { 203 | return models.User{}, "", errors.New("invalid webhook") 204 | } 205 | // checking if the signature is valid 206 | // Validating the signature 207 | 208 | h := hmac.New(sha256.New, []byte(webhook.Secret)) 209 | h.Write(data) 210 | expectedSignature := base64.StdEncoding.EncodeToString(h.Sum(nil)) 211 | 212 | if !hmac.Equal([]byte(signature), []byte(expectedSignature)) { 213 | return models.User{}, "", errors.New("invalid signature") 214 | } 215 | 216 | var user models.User 217 | res = a.db.Where("user_id = ?", webhook.Owner).Find(&user) 218 | if res.Error != nil { 219 | return models.User{}, "", res.Error 220 | } 221 | return user, webhook.Task, nil 222 | } 223 | 224 | func (a *AuthServiceImpl) ValidateWebhookSignatureCommon( 225 | id string, 226 | signature string, 227 | body []byte, 228 | ) (models.User, string, error) { 229 | var webhook models.Webhook 230 | res := a.db.Where("id = ?", id).Find(&webhook) 231 | if res.Error != nil { 232 | return models.User{}, "", res.Error 233 | } 234 | // checking if there's any result 235 | if res.RowsAffected == 0 { 236 | return models.User{}, "", errors.New("invalid webhook") 237 | } 238 | // checking if the signature is valid 239 | // Validating the signature 240 | 241 | h := hmac.New(sha512.New, []byte(webhook.Secret)) 242 | h.Write(body) 243 | expectedSignature := hex.EncodeToString(h.Sum(nil)) 244 | 245 | if !hmac.Equal([]byte(signature), []byte(expectedSignature)) { 246 | return models.User{}, "", errors.New("invalid signature") 247 | } 248 | 249 | var user models.User 250 | res = a.db.Where("user_id = ?", webhook.Owner).Find(&user) 251 | if res.Error != nil { 252 | return models.User{}, "", res.Error 253 | } 254 | return user, webhook.Task, nil 255 | } 256 | 257 | func (a *AuthServiceImpl) IsAutorised(auth *models.Authorization) (bool, error) { 258 | // Checking if the user owns the API token 259 | if auth.Resource == "apiTokens" { 260 | var apiToken models.ApiToken 261 | res := a.db.Where("id = ?", auth.ResourceID).Find(&apiToken) 262 | if res.Error != nil { 263 | return false, res.Error 264 | } 265 | 266 | if apiToken.Owner == auth.UserID { 267 | return true, nil 268 | } 269 | } 270 | 271 | roles, err := a.UserService.GetUserRoles(auth.UserID.String(), auth.Provider) 272 | if err != nil { 273 | log.Println(err) 274 | return false, err 275 | } 276 | for _, role := range roles { 277 | if role.Resource == "*" || role.Resource == auth.Resource && 278 | (len(role.Resource_IDs) > 0 && role.Resource_IDs[0] == "*" || 279 | slices.Contains(role.Resource_IDs, auth.ResourceID)) && 280 | (role.Access == auth.Access || role.Access == "write") { 281 | return true, nil 282 | } 283 | } 284 | 285 | return false, nil 286 | } 287 | 288 | func (a *AuthServiceImpl) GetAuthorizationList(auth *models.Authorization) ([]string, error) { 289 | roles, err := a.UserService.GetUserRoles(auth.UserID.String(), auth.Provider) 290 | if err != nil { 291 | log.Println(err) 292 | return []string{}, err 293 | } 294 | 295 | var authList []string 296 | for i := range roles { 297 | role := &roles[i] 298 | if role.Resource == "*" || role.Resource == auth.Resource { 299 | if role.Resource_IDs[0] == "*" { 300 | return []string{"*"}, nil 301 | } 302 | authList = append(authList, role.Resource_IDs...) 303 | } 304 | } 305 | 306 | return authList, nil 307 | } 308 | -------------------------------------------------------------------------------- /services/cronjobs_svc.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/kriten-io/kriten/config" 8 | "github.com/kriten-io/kriten/helpers" 9 | "github.com/kriten-io/kriten/models" 10 | 11 | "encoding/json" 12 | "strings" 13 | 14 | "github.com/go-openapi/spec" 15 | "github.com/go-openapi/strfmt" 16 | "github.com/go-openapi/validate" 17 | corev1 "k8s.io/api/core/v1" 18 | kerrors "k8s.io/apimachinery/pkg/api/errors" 19 | ) 20 | 21 | type CronJobService interface { 22 | ListCronJobs([]string) ([]models.CronJob, error) 23 | GetCronJob(string) (models.CronJob, error) 24 | CreateCronJob(models.CronJob) (models.CronJob, error) 25 | UpdateCronJob(models.CronJob) (models.CronJob, error) 26 | DeleteCronJob(string) error 27 | GetSchema(string) (map[string]interface{}, error) 28 | } 29 | 30 | type CronJobServiceImpl struct { 31 | config config.Config 32 | } 33 | 34 | func NewCronJobService(config config.Config) CronJobService { 35 | return &CronJobServiceImpl{ 36 | config: config, 37 | } 38 | } 39 | 40 | func (j *CronJobServiceImpl) ListCronJobs(authList []string) ([]models.CronJob, error) { 41 | var jobsList []models.CronJob 42 | var labelSelector []string 43 | 44 | if len(authList) == 0 { 45 | return jobsList, nil 46 | } 47 | 48 | if authList[0] != "*" { 49 | for _, s := range authList { 50 | labelSelector = append(labelSelector, "task-name="+s) 51 | } 52 | } 53 | 54 | jobs, err := helpers.ListCronJobs(j.config.Kube, labelSelector) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | for _, job := range jobs.Items { 60 | var data map[string]interface{} 61 | // This unmarshal is only used to fetch the extra vars, it doesn't look very reliable so it might need a rework 62 | containerEnv := job.Spec.JobTemplate.Spec.Template.Spec.Containers[0].Env 63 | if len(containerEnv) > 0 { 64 | err = json.Unmarshal([]byte(containerEnv[0].Value), &data) 65 | if err != nil { 66 | return nil, err 67 | } 68 | } 69 | jobRet := models.CronJob{ 70 | Name: job.Name, 71 | Owner: job.Spec.JobTemplate.Spec.Template.Labels["owner"], 72 | Task: job.Spec.JobTemplate.Spec.Template.Labels["task-name"], 73 | Schedule: job.Spec.Schedule, 74 | Disable: *job.Spec.Suspend, 75 | ExtraVars: data, 76 | } 77 | jobsList = append(jobsList, jobRet) 78 | } 79 | 80 | return jobsList, nil 81 | } 82 | 83 | func (j *CronJobServiceImpl) GetCronJob(name string) (models.CronJob, error) { 84 | var cronjob models.CronJob 85 | 86 | job, err := helpers.GetCronJob(j.config.Kube, name) 87 | if err != nil { 88 | return cronjob, err 89 | } 90 | 91 | var data map[string]interface{} 92 | // This unmarshal is only used to fetch the extra vars, it doesn't look very reliable so it might need a rework 93 | containerEnv := job.Spec.JobTemplate.Spec.Template.Spec.Containers[0].Env 94 | if len(containerEnv) > 0 { 95 | err = json.Unmarshal([]byte(containerEnv[0].Value), &data) 96 | if err != nil { 97 | return cronjob, err 98 | } 99 | } 100 | cronjob = models.CronJob{ 101 | Name: job.Name, 102 | Owner: job.Spec.JobTemplate.Spec.Template.Labels["owner"], 103 | Task: job.Spec.JobTemplate.Spec.Template.Labels["task-name"], 104 | Schedule: job.Spec.Schedule, 105 | Disable: *job.Spec.Suspend, 106 | ExtraVars: data, 107 | } 108 | 109 | return cronjob, nil 110 | } 111 | 112 | func (j *CronJobServiceImpl) CreateCronJob(cronjob models.CronJob) (models.CronJob, error) { 113 | runner, command, err := PreFlightChecks(j.config.Kube, cronjob) 114 | if err != nil { 115 | return models.CronJob{}, err 116 | } 117 | 118 | _, err = helpers.CreateOrUpdateCronJob(j.config.Kube, cronjob, runner, command, "create") 119 | 120 | return cronjob, err 121 | } 122 | 123 | func (j *CronJobServiceImpl) UpdateCronJob(cronjob models.CronJob) (models.CronJob, error) { 124 | runner, command, err := PreFlightChecks(j.config.Kube, cronjob) 125 | if err != nil { 126 | return models.CronJob{}, err 127 | } 128 | 129 | _, err = helpers.CreateOrUpdateCronJob(j.config.Kube, cronjob, runner, command, "update") 130 | 131 | return cronjob, err 132 | } 133 | 134 | func (j *CronJobServiceImpl) DeleteCronJob(id string) error { 135 | err := helpers.DeleteCronJob(j.config.Kube, id) 136 | if err != nil { 137 | return err 138 | } 139 | 140 | return nil 141 | } 142 | 143 | func (j *CronJobServiceImpl) GetSchema(name string) (map[string]interface{}, error) { 144 | var data map[string]interface{} 145 | 146 | configMap, err := helpers.GetConfigMap(j.config.Kube, name) 147 | if err != nil { 148 | return nil, err 149 | } 150 | if configMap.Data["runner"] == "" { 151 | return nil, fmt.Errorf("task %s not found", name) 152 | } 153 | 154 | if configMap.Data["schema"] != "" { 155 | err = json.Unmarshal([]byte(configMap.Data["schema"]), &data) 156 | if err != nil { 157 | return nil, err 158 | } 159 | } 160 | 161 | return data, nil 162 | } 163 | 164 | func PreFlightChecks(kube config.KubeConfig, cronjob models.CronJob) (*corev1.ConfigMap, string, error) { 165 | task, err := helpers.GetConfigMap(kube, cronjob.Task) 166 | if err != nil { 167 | return nil, "", err 168 | } 169 | 170 | if task.Data["schema"] != "" { 171 | schema := new(spec.Schema) 172 | _ = json.Unmarshal([]byte(task.Data["schema"]), schema) 173 | 174 | // strfmt.Default is the registry of recognized formats 175 | err = validate.AgainstSchema(schema, cronjob.ExtraVars, strfmt.Default) 176 | if err != nil { 177 | log.Printf("JSON does not validate against schema: %v", err) 178 | return nil, "", err 179 | } 180 | } 181 | 182 | runner, err := helpers.GetConfigMap(kube, task.Data["runner"]) 183 | if err != nil { 184 | return nil, "", err 185 | } 186 | 187 | if runner.Data["branch"] == "" { 188 | runner.Data["branch"] = "main" 189 | } 190 | 191 | secret, err := helpers.GetSecret(kube, task.Data["runner"]) 192 | if err != nil { 193 | if !kerrors.IsNotFound(err) { 194 | return nil, "", err 195 | } 196 | } else { 197 | gitToken := string(secret.Data["token"]) 198 | if gitToken != "" { 199 | runner.Data["gitURL"] = strings.Replace(runner.Data["gitURL"], "://", "://"+gitToken+":@", 1) 200 | } 201 | } 202 | 203 | return runner, task.Data["command"], nil 204 | } 205 | -------------------------------------------------------------------------------- /services/groups_svc.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/kriten-io/kriten/config" 8 | "github.com/kriten-io/kriten/models" 9 | 10 | "golang.org/x/exp/slices" 11 | 12 | "gorm.io/gorm" 13 | ) 14 | 15 | type GroupService interface { 16 | ListGroups([]string) ([]models.Group, error) 17 | GetGroup(string) (models.Group, error) 18 | GetUserGroups(string) ([]models.UserGroup, error) 19 | GetGroupByID(string) (models.Group, error) 20 | CreateGroup(models.Group) (models.Group, error) 21 | UpdateGroup(models.Group) (models.Group, error) 22 | ListUsersInGroup(string) ([]models.GroupUser, error) 23 | AddUsersToGroup(string, []models.GroupUser) (models.Group, error) 24 | RemoveUsersFromGroup(string, []models.GroupUser) (models.Group, error) 25 | UpdateUsers([]models.GroupUser, string, string) ([]string, error) 26 | DeleteGroup(string) error 27 | GetGroupRoles(string, string) ([]models.Role, error) 28 | } 29 | 30 | type GroupServiceImpl struct { 31 | db *gorm.DB 32 | UserService UserService 33 | config config.Config 34 | } 35 | 36 | func NewGroupService(database *gorm.DB, us UserService, config config.Config) GroupService { 37 | return &GroupServiceImpl{ 38 | db: database, 39 | UserService: us, 40 | config: config, 41 | } 42 | } 43 | 44 | func (g *GroupServiceImpl) ListGroups(authList []string) ([]models.Group, error) { 45 | var groups []models.Group 46 | var res *gorm.DB 47 | 48 | log.Println(authList) 49 | 50 | if len(authList) == 0 { 51 | return groups, nil 52 | } 53 | 54 | if slices.Contains(authList, "*") { 55 | res = g.db.Find(&groups) 56 | } else { 57 | res = g.db.Find(&groups, authList) 58 | } 59 | if res.Error != nil { 60 | return groups, res.Error 61 | } 62 | 63 | return groups, nil 64 | } 65 | 66 | func (g *GroupServiceImpl) GetGroup(name string) (models.Group, error) { 67 | var group models.Group 68 | res := g.db.Where("name = ?", name).Find(&group) 69 | if res.Error != nil { 70 | return models.Group{}, res.Error 71 | } 72 | 73 | if group.Name == "" { 74 | return models.Group{}, fmt.Errorf("group %s not found, please check name", name) 75 | } 76 | 77 | return group, nil 78 | } 79 | 80 | func (g *GroupServiceImpl) GetGroupByID(id string) (models.Group, error) { 81 | var group models.Group 82 | res := g.db.Where("group_id = ?", id).Find(&group) 83 | if res.Error != nil { 84 | return models.Group{}, res.Error 85 | } 86 | if group.Name == "" { 87 | return models.Group{}, fmt.Errorf("group %s not found, please check id", id) 88 | } 89 | return group, nil 90 | } 91 | 92 | func (g *GroupServiceImpl) CreateGroup(group models.Group) (models.Group, error) { 93 | res := g.db.Create(&group) 94 | 95 | return group, res.Error 96 | } 97 | 98 | func (g *GroupServiceImpl) UpdateGroup(group models.Group) (models.Group, error) { 99 | res := g.db.Updates(group) 100 | if res.Error != nil { 101 | return models.Group{}, res.Error 102 | } 103 | 104 | newGroup, err := g.GetGroup(group.Name) 105 | if err != nil { 106 | return models.Group{}, err 107 | } 108 | return newGroup, nil 109 | } 110 | 111 | func (g *GroupServiceImpl) GetUserGroups(id string) ([]models.UserGroup, error) { 112 | var user models.User 113 | var groups []models.UserGroup 114 | res := g.db.Where("user_id = ?", id).Find(&user) 115 | if res.Error != nil { 116 | return []models.UserGroup{}, res.Error 117 | } 118 | 119 | if user.Username == "" { 120 | return []models.UserGroup{}, fmt.Errorf("user %s not found, please check uuid", id) 121 | } 122 | 123 | for _, groupID := range user.Groups { 124 | group, err := g.GetGroupByID(groupID) 125 | if err != nil { 126 | return []models.UserGroup{}, err 127 | } 128 | groups = append(groups, models.UserGroup{ 129 | ID: group.ID, 130 | Name: group.Name, 131 | Provider: group.Provider, 132 | }) 133 | } 134 | 135 | return groups, nil 136 | } 137 | 138 | func (g *GroupServiceImpl) ListUsersInGroup(id string) ([]models.GroupUser, error) { 139 | var users []models.GroupUser 140 | 141 | group, err := g.GetGroupByID(id) 142 | if err != nil { 143 | return nil, err 144 | } 145 | 146 | for _, userID := range group.Users { 147 | user, err := g.UserService.GetUser(userID) 148 | if err != nil { 149 | return nil, err 150 | } 151 | users = append(users, models.GroupUser{ 152 | Username: user.Username, 153 | Provider: user.Provider, 154 | ID: user.ID, 155 | }) 156 | } 157 | 158 | return users, nil 159 | } 160 | 161 | func (g *GroupServiceImpl) AddUsersToGroup(id string, users []models.GroupUser) (models.Group, error) { 162 | group, err := g.GetGroupByID(id) 163 | if err != nil { 164 | return models.Group{}, err 165 | } 166 | 167 | usersID, err := g.UpdateUsers(users, group.ID.String(), "add") 168 | if err != nil { 169 | return models.Group{}, err 170 | } 171 | 172 | group.Users = RemoveDuplicates(append(group.Users, usersID...)) 173 | 174 | newGroup, err := g.UpdateGroup(group) 175 | if err != nil { 176 | return models.Group{}, err 177 | } 178 | 179 | return newGroup, nil 180 | } 181 | 182 | func (g *GroupServiceImpl) RemoveUsersFromGroup(id string, users []models.GroupUser) (models.Group, error) { 183 | group, err := g.GetGroupByID(id) 184 | if err != nil { 185 | return models.Group{}, err 186 | } 187 | 188 | usersID, err := g.UpdateUsers(users, group.ID.String(), "remove") 189 | if err != nil { 190 | return models.Group{}, err 191 | } 192 | 193 | group.Users = RemoveFromSlice(group.Users, usersID) 194 | 195 | newGroup, err := g.UpdateGroup(group) 196 | if err != nil { 197 | return models.Group{}, err 198 | } 199 | 200 | return newGroup, nil 201 | } 202 | 203 | func (g *GroupServiceImpl) DeleteGroup(id string) error { 204 | group, err := g.GetGroupByID(id) 205 | if err != nil { 206 | return err 207 | } 208 | 209 | if group.Users != nil { 210 | return fmt.Errorf("cannot delete group %s, please remove users first", group.Name) 211 | } 212 | 213 | return g.db.Unscoped().Delete(&group).Error 214 | } 215 | 216 | func (g *GroupServiceImpl) GetGroupRoles(subjectID string, provider string) ([]models.Role, error) { 217 | var roles []models.Role 218 | 219 | // SELECT * 220 | // FROM roles 221 | // INNER JOIN role_bindings 222 | // ON roles.role_id = role_bindings.role_id 223 | // WHERE role_bindings.subject_provider = provider AND role_bindings.subject_id = subjectID; 224 | res := g.db.Model(&models.Role{}).Joins( 225 | "left join role_bindings on roles.role_id = role_bindings.role_id").Where( 226 | "role_bindings.subject_provider = ? AND role_bindings.subject_id = ?", provider, subjectID).Find(&roles) 227 | if res.Error != nil { 228 | return []models.Role{}, res.Error 229 | } 230 | 231 | return roles, nil 232 | } 233 | 234 | func (g *GroupServiceImpl) UpdateUsers(users []models.GroupUser, groupID string, operation string) ([]string, error) { 235 | var usersID []string 236 | 237 | for _, u := range users { 238 | user, err := g.UserService.GetByUsernameAndProvider(u.Username, u.Provider) 239 | if err != nil { 240 | return nil, err 241 | } 242 | usersID = append(usersID, user.ID.String()) 243 | 244 | if operation == "add" { 245 | _, err = g.UserService.AddGroup(user, groupID) 246 | } else { 247 | _, err = g.UserService.RemoveGroup(user, groupID) 248 | } 249 | if err != nil { 250 | return nil, err 251 | } 252 | } 253 | 254 | return usersID, nil 255 | } 256 | 257 | func RemoveDuplicates(strSlice []string) []string { 258 | allKeys := make(map[string]bool) 259 | list := []string{} 260 | for _, item := range strSlice { 261 | if _, value := allKeys[item]; !value { 262 | allKeys[item] = true 263 | list = append(list, item) 264 | } 265 | } 266 | return list 267 | } 268 | 269 | func RemoveFromSlice(groupUsers []string, users []string) []string { 270 | for key, value := range groupUsers { 271 | if slices.Contains(users, value) { 272 | groupUsers = append(groupUsers[:key], groupUsers[key+1:]...) 273 | } 274 | } 275 | return groupUsers 276 | } 277 | -------------------------------------------------------------------------------- /services/job_svc.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "sort" 7 | "time" 8 | 9 | "github.com/kriten-io/kriten/config" 10 | "github.com/kriten-io/kriten/helpers" 11 | "github.com/kriten-io/kriten/models" 12 | 13 | "encoding/json" 14 | "strings" 15 | 16 | "github.com/go-errors/errors" 17 | "github.com/go-openapi/spec" 18 | "github.com/go-openapi/strfmt" 19 | "github.com/go-openapi/validate" 20 | kerrors "k8s.io/apimachinery/pkg/api/errors" 21 | "k8s.io/apimachinery/pkg/util/wait" 22 | ) 23 | 24 | type JobService interface { 25 | ListJobs([]string) ([]models.Job, error) 26 | GetJob(string, string) (models.Job, error) 27 | GetLog(string, string) (string, error) 28 | CreateJob(string, string, string) (models.Job, error) 29 | GetSchema(string) (map[string]interface{}, error) 30 | } 31 | 32 | type JobServiceImpl struct { 33 | config config.Config 34 | } 35 | 36 | func NewJobService(config config.Config) JobService { 37 | return &JobServiceImpl{ 38 | config: config, 39 | } 40 | } 41 | 42 | func findDelimitedString(str string) ([]byte, error) { 43 | delimiter := "^JSON" 44 | var match []byte 45 | index := strings.Index(str, delimiter) 46 | 47 | if index == -1 { 48 | return match, nil 49 | } 50 | 51 | index += len(delimiter) 52 | 53 | for { 54 | char := str[index] 55 | 56 | if strings.HasPrefix(str[index:index+len(delimiter)], delimiter) { 57 | break 58 | } 59 | 60 | match = append(match, char) 61 | index++ 62 | 63 | if index+len(delimiter) >= len(str) { 64 | match = nil 65 | break 66 | } 67 | } 68 | 69 | return match, nil 70 | } 71 | 72 | func (j *JobServiceImpl) ListJobs(authList []string) ([]models.Job, error) { 73 | var jobsList []models.Job 74 | var labelSelector []string 75 | 76 | if len(authList) == 0 { 77 | return jobsList, nil 78 | } 79 | 80 | if authList[0] != "*" { 81 | for _, s := range authList { 82 | labelSelector = append(labelSelector, "task-name="+s) 83 | } 84 | } 85 | // labelSelector := "task-name=" + taskName 86 | // if username != "" { 87 | // labelSelector = labelSelector + ",owner=" + username 88 | // } 89 | 90 | jobs, err := helpers.ListJobs(j.config.Kube, labelSelector) 91 | if err != nil { 92 | return nil, err 93 | } 94 | 95 | if len(jobs.Items) != 0 { 96 | sort.SliceStable(jobs.Items, func(i, j int) bool { 97 | return jobs.Items[i].Status.StartTime.After(jobs.Items[j].Status.StartTime.Time) 98 | }) 99 | } 100 | 101 | for i := range jobs.Items { 102 | job := &jobs.Items[i] 103 | var jobRet models.Job 104 | jobRet.ID = job.Name 105 | jobRet.Owner = job.Labels["owner"] 106 | jobRet.StartTime = job.Status.StartTime.Format(time.UnixDate) 107 | if job.Status.CompletionTime != nil { 108 | jobRet.CompletionTime = job.Status.CompletionTime.Format(time.UnixDate) 109 | } 110 | jobRet.Failed = job.Status.Failed 111 | jobRet.Completed = job.Status.Succeeded 112 | jobsList = append(jobsList, jobRet) 113 | } 114 | 115 | return jobsList, nil 116 | } 117 | 118 | func (j *JobServiceImpl) GetJob(username string, jobID string) (models.Job, error) { 119 | var jobStatus models.Job 120 | 121 | labelSelector := fmt.Sprintf("job-name=%s", jobID) 122 | if username != "" { 123 | labelSelector = labelSelector + ",owner=" + username 124 | } 125 | 126 | pods, err := helpers.ListPods(j.config.Kube, labelSelector) 127 | if err != nil { 128 | return jobStatus, err 129 | } 130 | 131 | if len(pods.Items) == 0 { 132 | return jobStatus, errors.New("no pods found - check job ID") 133 | } 134 | 135 | for i := range pods.Items { 136 | for c := range pods.Items[i].Status.InitContainerStatuses { 137 | switch { 138 | case pods.Items[i].Status.InitContainerStatuses[c].State.Terminated.Reason == "ImagePullBackOff": 139 | return jobStatus, errors.New("failed to pull init container image from container registry.") 140 | case pods.Items[i].Status.InitContainerStatuses[c].State.Terminated.Reason == "Error": 141 | return jobStatus, errors.New("failed to clone repo: wrong repo url or incorrect credentials.") 142 | } 143 | } 144 | for c := range pods.Items[i].Status.ContainerStatuses { 145 | state := pods.Items[i].Status.ContainerStatuses[c].State 146 | // adding check if application container is not running because it cannot pull image 147 | // more checks to be added to cover any other cases 148 | if state.Waiting != nil { 149 | if pods.Items[i].Status.ContainerStatuses[c].State.Waiting.Reason == "ImagePullBackOff" { 150 | return jobStatus, errors.New("failed to pull application container image from container registry.") 151 | } 152 | } 153 | } 154 | } 155 | 156 | job, err := helpers.GetJob(j.config.Kube, jobID) 157 | 158 | if err != nil { 159 | return jobStatus, err 160 | } 161 | 162 | jobStatus.ID = job.Name 163 | jobStatus.Owner = job.Labels["owner"] 164 | jobStatus.StartTime = job.Status.StartTime.Format(time.UnixDate) 165 | if job.Status.CompletionTime != nil { 166 | jobStatus.CompletionTime = job.Status.CompletionTime.Format(time.UnixDate) 167 | } 168 | jobStatus.Failed = job.Status.Failed 169 | jobStatus.Completed = job.Status.Succeeded 170 | 171 | jobLog, err := j.GetLog(username, jobID) 172 | if err != nil { 173 | jobStatus.Stdout += fmt.Sprintf("failed to read logs from containers: %v", err) 174 | } else { 175 | jobStatus.Stdout += jobLog 176 | } 177 | 178 | if jobStatus.Stdout != "" { 179 | json_byte, _ := findDelimitedString(jobStatus.Stdout) 180 | 181 | if json_byte != nil { 182 | // ^JSON delimited text found in the log 183 | 184 | replacer := strings.NewReplacer("\n", "", "\\", "") 185 | json_string := replacer.Replace(string(json_byte)) 186 | 187 | if err := json.Unmarshal([]byte(json_string), &jobStatus.JsonData); err != nil { 188 | jobStatus.JsonData = map[string]interface{}{"error": "failed to parse JSON"} 189 | return jobStatus, nil 190 | } 191 | } 192 | } 193 | 194 | return jobStatus, nil 195 | } 196 | 197 | func (j *JobServiceImpl) GetLog(username string, jobID string) (string, error) { 198 | var logs string 199 | 200 | labelSelector := "job-name=" + jobID 201 | if username != "" { 202 | labelSelector = labelSelector + ",owner=" + username 203 | } 204 | 205 | pods, err := helpers.ListPods(j.config.Kube, labelSelector) 206 | if err != nil { 207 | return logs, err 208 | } 209 | 210 | if len(pods.Items) == 0 { 211 | return logs, errors.New("no pods found - check job ID") 212 | } 213 | 214 | for _, pod := range pods.Items { 215 | // TODO: this will only retrieve logs for now, can be extended if needed 216 | logs += "\n\n## init container logs\n" 217 | for c := range pod.Spec.InitContainers { 218 | jobLog, err := helpers.GetLogs(j.config.Kube, pod.Name, pod.Spec.InitContainers[c].Name) 219 | if err != nil { 220 | logs += fmt.Sprintf("error reading logs from init container: %v", err) 221 | } else { 222 | logs += jobLog 223 | } 224 | } 225 | // resetting jobLog to avoid duplications 226 | logs += "\n\n##application container logs \n" 227 | for c := range pod.Spec.Containers { 228 | jobLog, err := helpers.GetLogs(j.config.Kube, pod.Name, pod.Spec.Containers[c].Name) 229 | if err != nil { 230 | logs += fmt.Sprintf("error reading logs from application container: %v", err) 231 | } else { 232 | logs += jobLog 233 | } 234 | } 235 | } 236 | 237 | return logs, nil 238 | } 239 | 240 | func (j *JobServiceImpl) CreateJob(username string, taskName string, extraVars string) (models.Job, error) { 241 | var jobStatus models.Job 242 | 243 | task, err := helpers.GetConfigMap(j.config.Kube, taskName) 244 | if err != nil { 245 | return jobStatus, err 246 | } 247 | runnerName := task.Data["runner"] 248 | 249 | if task.Data["schema"] != "" { 250 | schema := new(spec.Schema) 251 | _ = json.Unmarshal([]byte(task.Data["schema"]), schema) 252 | 253 | input := map[string]interface{}{} 254 | 255 | // JSON data to validate 256 | _ = json.Unmarshal([]byte(extraVars), &input) 257 | 258 | // strfmt.Default is the registry of recognized formats 259 | err = validate.AgainstSchema(schema, input, strfmt.Default) 260 | if err != nil { 261 | log.Printf("JSON does not validate against schema: %v", err) 262 | return models.Job{}, err 263 | } 264 | } 265 | 266 | runner, err := helpers.GetConfigMap(j.config.Kube, runnerName) 267 | if err != nil { 268 | return jobStatus, err 269 | } 270 | runnerImage := runner.Data["image"] 271 | gitURL := runner.Data["gitURL"] 272 | gitBranch := runner.Data["branch"] 273 | 274 | if gitBranch == "" { 275 | gitBranch = "main" 276 | } 277 | tokenObjName := runnerName + "-token" 278 | token, err := helpers.GetSecret(j.config.Kube, tokenObjName) 279 | if err != nil { 280 | if !kerrors.IsNotFound(err) { 281 | return jobStatus, err 282 | } 283 | } else { 284 | gitToken := string(token.Data["token"]) 285 | if gitToken != "" { 286 | gitURL = strings.Replace(gitURL, "://", "://"+gitToken+":@", 1) 287 | } 288 | } 289 | 290 | jobID, err := helpers.CreateJob( 291 | j.config.Kube, 292 | taskName, 293 | runnerName, 294 | runnerImage, 295 | username, 296 | extraVars, 297 | task.Data["command"], 298 | gitURL, 299 | gitBranch, 300 | ) 301 | 302 | jobStatus.ID = jobID 303 | 304 | if err != nil { 305 | return jobStatus, err 306 | } 307 | 308 | if task.Data["synchronous"] == "true" { 309 | _ = wait.Poll(100*time.Millisecond, 20*time.Second, func() (done bool, err error) { 310 | 311 | job, err := helpers.GetJob(j.config.Kube, jobID) 312 | 313 | if err != nil { 314 | fmt.Println(err) 315 | return false, err 316 | } 317 | 318 | if job.Status.Succeeded != 0 || job.Status.Failed != 0 { 319 | return true, nil 320 | } 321 | 322 | return false, nil 323 | }) 324 | 325 | ret, err := j.GetJob(username, jobID) 326 | return ret, err 327 | } 328 | 329 | return jobStatus, nil 330 | } 331 | 332 | func (j *JobServiceImpl) GetSchema(name string) (map[string]interface{}, error) { 333 | var data map[string]interface{} 334 | 335 | configMap, err := helpers.GetConfigMap(j.config.Kube, name) 336 | if err != nil { 337 | return nil, err 338 | } 339 | if configMap.Data["runner"] == "" { 340 | return nil, fmt.Errorf("task %s not found", name) 341 | } 342 | 343 | if configMap.Data["schema"] != "" { 344 | err = json.Unmarshal([]byte(configMap.Data["schema"]), &data) 345 | if err != nil { 346 | return nil, err 347 | } 348 | } 349 | 350 | return data, nil 351 | } 352 | -------------------------------------------------------------------------------- /services/role_bindings_svc.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/kriten-io/kriten/config" 9 | "github.com/kriten-io/kriten/models" 10 | 11 | uuid "github.com/satori/go.uuid" 12 | "golang.org/x/exp/slices" 13 | 14 | "gorm.io/gorm" 15 | ) 16 | 17 | type RoleBindingService interface { 18 | ListRoleBindings([]string, map[string]string) ([]models.RoleBinding, error) 19 | GetRoleBinding(string) (models.RoleBinding, error) 20 | CreateRoleBinding(models.RoleBinding) (models.RoleBinding, error) 21 | UpdateRoleBinding(models.RoleBinding) (models.RoleBinding, error) 22 | DeleteRoleBinding(string) error 23 | CheckRoleBinding(models.RoleBinding) (uuid.UUID, uuid.UUID, error) 24 | } 25 | 26 | type RoleBindingServiceImpl struct { 27 | RoleService RoleService 28 | GroupService GroupService 29 | db *gorm.DB 30 | config config.Config 31 | } 32 | 33 | func NewRoleBindingService(db *gorm.DB, config config.Config, rs RoleService, gs GroupService) RoleBindingService { 34 | return &RoleBindingServiceImpl{ 35 | db: db, 36 | config: config, 37 | RoleService: rs, 38 | GroupService: gs, 39 | } 40 | } 41 | 42 | func (r *RoleBindingServiceImpl) ListRoleBindings( 43 | authList []string, 44 | filters map[string]string, 45 | ) ([]models.RoleBinding, error) { 46 | var roleBindings []models.RoleBinding 47 | var res *gorm.DB 48 | 49 | if len(authList) == 0 { 50 | return roleBindings, nil 51 | } else if slices.Contains(authList, "*") { 52 | res = r.db.Where(filters).Find(&roleBindings) 53 | } else { 54 | res = r.db.Where(filters).Find(&roleBindings, authList) 55 | } 56 | if res.Error != nil { 57 | return roleBindings, res.Error 58 | } 59 | 60 | return roleBindings, nil 61 | } 62 | 63 | func (r *RoleBindingServiceImpl) GetRoleBinding(id string) (models.RoleBinding, error) { 64 | var role models.RoleBinding 65 | res := r.db.Where("name = ?", id).Find(&role) 66 | if res.Error != nil { 67 | return models.RoleBinding{}, res.Error 68 | } 69 | 70 | if role.Name == "" { 71 | return models.RoleBinding{}, fmt.Errorf("role_binding %s not found, please check id", id) 72 | } 73 | 74 | return role, nil 75 | } 76 | 77 | func (r *RoleBindingServiceImpl) CreateRoleBinding(roleBinding models.RoleBinding) (models.RoleBinding, error) { 78 | roleID, subjectID, err := r.CheckRoleBinding(roleBinding) 79 | if err != nil { 80 | return roleBinding, err 81 | } 82 | roleBinding.RoleID = roleID 83 | roleBinding.SubjectID = subjectID 84 | 85 | res := r.db.Create(&roleBinding) 86 | 87 | return roleBinding, res.Error 88 | } 89 | 90 | func (r *RoleBindingServiceImpl) UpdateRoleBinding(roleBinding models.RoleBinding) (models.RoleBinding, error) { 91 | roleID, subjectID, err := r.CheckRoleBinding(roleBinding) 92 | if err != nil { 93 | return roleBinding, err 94 | } 95 | roleBinding.RoleID = roleID 96 | roleBinding.SubjectID = subjectID 97 | res := r.db.Updates(roleBinding) 98 | if res.Error != nil { 99 | return models.RoleBinding{}, res.Error 100 | } 101 | 102 | newRoleBinding, err := r.GetRoleBinding(roleBinding.ID.String()) 103 | if err != nil { 104 | return models.RoleBinding{}, err 105 | } 106 | return newRoleBinding, nil 107 | } 108 | 109 | func (r *RoleBindingServiceImpl) DeleteRoleBinding(id string) error { 110 | roleBinding, err := r.GetRoleBinding(id) 111 | if err != nil { 112 | return err 113 | } 114 | 115 | if roleBinding.Builtin { 116 | return errors.New("cannot delete builtin resource") 117 | } 118 | 119 | return r.db.Unscoped().Delete(&roleBinding).Error 120 | } 121 | 122 | func (r *RoleBindingServiceImpl) CheckRoleBinding(roleBinding models.RoleBinding) (uuid.UUID, uuid.UUID, error) { 123 | role, err := r.RoleService.GetRole(roleBinding.RoleName) 124 | if err != nil { 125 | log.Println(err) 126 | return uuid.UUID{}, uuid.UUID{}, err 127 | } 128 | 129 | var group models.Group 130 | if roleBinding.SubjectKind == "groups" { 131 | group, err = r.GroupService.GetGroup(roleBinding.SubjectName) 132 | if err != nil { 133 | log.Println(err) 134 | return uuid.UUID{}, uuid.UUID{}, err 135 | } 136 | } else { 137 | return uuid.UUID{}, uuid.UUID{}, fmt.Errorf("subject_kind not valid") 138 | } 139 | 140 | return role.ID, group.ID, nil 141 | } 142 | -------------------------------------------------------------------------------- /services/roles_svc.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/kriten-io/kriten/config" 8 | "github.com/kriten-io/kriten/helpers" 9 | "github.com/kriten-io/kriten/models" 10 | 11 | "golang.org/x/exp/slices" 12 | 13 | "gorm.io/gorm" 14 | ) 15 | 16 | type RoleService interface { 17 | ListRoles([]string) ([]models.Role, error) 18 | GetRole(string) (models.Role, error) 19 | CreateRole(models.Role) (models.Role, error) 20 | UpdateRole(models.Role) (models.Role, error) 21 | DeleteRole(string) error 22 | } 23 | 24 | type RoleServiceImpl struct { 25 | db *gorm.DB 26 | config config.Config 27 | RoleBindingService *RoleBindingService 28 | UserService *UserService 29 | } 30 | 31 | func NewRoleService(database *gorm.DB, config config.Config, rbs *RoleBindingService, us *UserService) RoleService { 32 | return &RoleServiceImpl{ 33 | db: database, 34 | config: config, 35 | RoleBindingService: rbs, 36 | UserService: us, 37 | } 38 | } 39 | 40 | func (r *RoleServiceImpl) ListRoles(authList []string) ([]models.Role, error) { 41 | var roles []models.Role 42 | var res *gorm.DB 43 | 44 | if len(authList) == 0 { 45 | return roles, nil 46 | } else if slices.Contains(authList, "*") { 47 | res = r.db.Find(&roles) 48 | } else { 49 | res = r.db.Find(&roles, authList) 50 | } 51 | if res.Error != nil { 52 | return roles, res.Error 53 | } 54 | 55 | return roles, nil 56 | } 57 | 58 | func (r *RoleServiceImpl) GetRole(id string) (models.Role, error) { 59 | var role models.Role 60 | res := r.db.Where("name = ?", id).Find(&role) 61 | if res.Error != nil { 62 | return models.Role{}, res.Error 63 | } 64 | 65 | if role.Name == "" { 66 | return models.Role{}, fmt.Errorf("role %s not found, please check id", id) 67 | } 68 | 69 | return role, nil 70 | } 71 | 72 | func (r *RoleServiceImpl) CreateRole(role models.Role) (models.Role, error) { 73 | err := r.CheckRole(role) 74 | if err != nil { 75 | return role, err 76 | } 77 | res := r.db.Create(&role) 78 | 79 | return role, res.Error 80 | } 81 | 82 | func (r *RoleServiceImpl) UpdateRole(role models.Role) (models.Role, error) { 83 | err := r.CheckRole(role) 84 | if err != nil { 85 | return role, err 86 | } 87 | 88 | res := r.db.Updates(role) 89 | if res.Error != nil { 90 | return models.Role{}, res.Error 91 | } 92 | 93 | newRole, err := r.GetRole(role.ID.String()) 94 | if err != nil { 95 | return models.Role{}, err 96 | } 97 | return newRole, nil 98 | } 99 | 100 | func (r *RoleServiceImpl) DeleteRole(id string) error { 101 | rbs := *r.RoleBindingService 102 | roleBindings, err := rbs.ListRoleBindings([]string{"*"}, nil) 103 | if err != nil { 104 | return err 105 | } 106 | for _, r := range roleBindings { 107 | if r.RoleID.String() == id { 108 | return fmt.Errorf("role is bound via role_binding: %s , please delete that first", r.ID) 109 | } 110 | } 111 | 112 | role, err := r.GetRole(id) 113 | if err != nil { 114 | return err 115 | } 116 | 117 | if role.Builtin { 118 | return errors.New("cannot delete builtin resource") 119 | } 120 | 121 | return r.db.Unscoped().Delete(&role).Error 122 | } 123 | 124 | // TODO: This is very crowded and repetitive 125 | // might need a refactor in the future. 126 | func (r *RoleServiceImpl) CheckRole(role models.Role) error { 127 | if role.Resource == "users" { 128 | for _, user := range role.Resource_IDs { 129 | us := *r.UserService 130 | _, err := us.GetUser(user) 131 | if err != nil { 132 | return err 133 | } 134 | } 135 | } else if role.Resource == "roles" { 136 | for _, role := range role.Resource_IDs { 137 | _, err := r.GetRole(role) 138 | if err != nil { 139 | return err 140 | } 141 | } 142 | } else if role.Resource == "role_bindings" { 143 | for _, roleBindings := range role.Resource_IDs { 144 | rbs := *r.RoleBindingService 145 | _, err := rbs.GetRoleBinding(roleBindings) 146 | if err != nil { 147 | return err 148 | } 149 | } 150 | } else { 151 | for _, c := range role.Resource_IDs { 152 | configMap, err := helpers.GetConfigMap(r.config.Kube, c) 153 | if err != nil { 154 | return err 155 | } 156 | // configmaps are all stored in the same namespace, so we need to identify the resource 157 | if role.Resource == "runners" { 158 | if configMap.Data["image"] == "" { 159 | return fmt.Errorf("runner %s not found", c) 160 | } 161 | } else if role.Resource == "tasks" { 162 | if configMap.Data["runner"] == "" { 163 | return fmt.Errorf("task %s not found", c) 164 | } 165 | } else if role.Resource == "jobs" { 166 | if configMap.Data["runner"] == "" { 167 | return fmt.Errorf("job %s not found", c) 168 | } 169 | } 170 | } 171 | } 172 | 173 | return nil 174 | } 175 | -------------------------------------------------------------------------------- /services/runner_svc.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/kriten-io/kriten/config" 9 | "github.com/kriten-io/kriten/helpers" 10 | "github.com/kriten-io/kriten/models" 11 | 12 | "golang.org/x/exp/slices" 13 | "k8s.io/apimachinery/pkg/api/errors" 14 | ) 15 | 16 | type RunnerService interface { 17 | ListRunners([]string) ([]map[string]string, error) 18 | GetRunner(string) (*models.Runner, error) 19 | CreateRunner(models.Runner) (*models.Runner, error) 20 | UpdateRunner(models.Runner) (*models.Runner, error) 21 | DeleteRunner(string) error 22 | GetAdminGroups(string) (string, error) 23 | ListAllJobs() ([]models.Job, error) 24 | GetSecret(string) (map[string]string, error) 25 | UpdateSecret(string, map[string]string) (map[string]string, error) 26 | DeleteSecret(string) error 27 | } 28 | 29 | type RunnerServiceImpl struct { 30 | config config.Config 31 | } 32 | 33 | func NewRunnerService(config config.Config) RunnerService { 34 | return &RunnerServiceImpl{ 35 | config: config, 36 | } 37 | } 38 | 39 | func (r *RunnerServiceImpl) ListRunners(authList []string) ([]map[string]string, error) { 40 | var runnersList []map[string]string 41 | 42 | if len(authList) == 0 { 43 | return runnersList, nil 44 | } 45 | 46 | configMaps, err := helpers.ListConfigMaps(r.config.Kube) 47 | if err != nil { 48 | return nil, fmt.Errorf("failed to validate Kubernetes ConfigMap name: %w", err) 49 | } 50 | 51 | for _, configMap := range configMaps.Items { 52 | // TODO: we don't currently have a way to identify what is a Runner configmap so I'm checking if it has an Image field 53 | // This will be changed when runners will live in a separate namespace 54 | if configMap.Data["image"] != "" { 55 | if authList[0] != "*" { 56 | if slices.Contains(authList, configMap.Data["name"]) { 57 | runnersList = append(runnersList, configMap.Data) 58 | } 59 | continue 60 | } 61 | runnersList = append(runnersList, configMap.Data) 62 | } 63 | } 64 | 65 | return runnersList, nil 66 | } 67 | 68 | func (r *RunnerServiceImpl) GetRunner(name string) (*models.Runner, error) { 69 | var runnerData models.Runner 70 | configMap, err := helpers.GetConfigMap(r.config.Kube, name) 71 | 72 | if err != nil { 73 | return &runnerData, err 74 | } 75 | 76 | if configMap.Data["image"] == "" { 77 | return nil, fmt.Errorf("runner %s not found", name) 78 | } 79 | 80 | b, _ := json.Marshal(configMap.Data) 81 | _ = json.Unmarshal(b, &runnerData) 82 | 83 | tokenObjName := name + "-token" 84 | token, err := r.GetSecret(tokenObjName) 85 | if err != nil { 86 | if !errors.IsNotFound(err) { 87 | return &runnerData, err 88 | } 89 | } else { 90 | runnerData.Token = token["token"] 91 | } 92 | 93 | secretCleared, err := r.GetSecret(name) 94 | if err != nil { 95 | if !errors.IsNotFound(err) { 96 | return &runnerData, err 97 | } 98 | } else { 99 | runnerData.Secret = secretCleared 100 | } 101 | 102 | return &runnerData, nil 103 | } 104 | 105 | func (r *RunnerServiceImpl) CreateRunner(runner models.Runner) (*models.Runner, error) { 106 | err := helpers.ValidateK8sConfigMapName(runner.Name) 107 | if err != nil { 108 | return nil, fmt.Errorf("%w", err) 109 | } 110 | 111 | b, _ := json.Marshal(runner) 112 | var data map[string]string 113 | _ = json.Unmarshal(b, &data) 114 | delete(data, "token") 115 | delete(data, "secret") 116 | 117 | if data["branch"] == "" { 118 | data["branch"] = "main" 119 | } 120 | 121 | _, err = helpers.CreateOrUpdateConfigMap(r.config.Kube, data, "create") 122 | if err != nil { 123 | return nil, err 124 | } 125 | 126 | // runner contains two types of secrets: git repo token and custom secrets, to be stored 127 | // in separate k8s secrets. token will be stored under runner name, secrets as runner name + secrets. 128 | if runner.Token != "" { 129 | tokenObjName := runner.Name + "-token" 130 | token := make(map[string]string) 131 | token["token"] = runner.Token 132 | _, err = helpers.CreateOrUpdateSecret(r.config.Kube, tokenObjName, token, "create") 133 | 134 | if err != nil { 135 | return nil, err 136 | } 137 | } 138 | 139 | if runner.Secret != nil { 140 | _, err = r.UpdateSecret(runner.Name, runner.Secret) 141 | 142 | if err != nil { 143 | return nil, err 144 | } 145 | } 146 | 147 | runnerData, err := r.GetRunner(runner.Name) 148 | return runnerData, err 149 | } 150 | 151 | func (r *RunnerServiceImpl) UpdateRunner(runner models.Runner) (*models.Runner, error) { 152 | _, err := helpers.GetConfigMap(r.config.Kube, runner.Name) 153 | if err != nil { 154 | return nil, err 155 | } 156 | 157 | b, _ := json.Marshal(runner) 158 | var data map[string]string 159 | _ = json.Unmarshal(b, &data) 160 | delete(data, "token") 161 | delete(data, "secret") 162 | 163 | _, err = helpers.CreateOrUpdateConfigMap(r.config.Kube, data, "update") 164 | if err != nil { 165 | return nil, err 166 | } 167 | 168 | tokenObjName := runner.Name + "-token" 169 | if runner.Token != "" && runner.Token != "************" { 170 | token := make(map[string]string) 171 | token["token"] = runner.Token 172 | operation := "update" 173 | // default operation is 'update', try to get the Secret first: if it's not found we need to create it 174 | // e.g. Someone created a Task without a secret and is adding one with update 175 | _, err = r.GetSecret(tokenObjName) 176 | if err != nil { 177 | if errors.IsNotFound(err) { 178 | operation = "create" 179 | } else { 180 | return nil, err 181 | } 182 | } 183 | _, err := helpers.CreateOrUpdateSecret(r.config.Kube, tokenObjName, token, operation) 184 | if err != nil { 185 | return nil, err 186 | } 187 | } else if runner.Token == "" { 188 | err = helpers.DeleteSecret(r.config.Kube, tokenObjName) 189 | if err != nil && !errors.IsNotFound(err) { 190 | return nil, err 191 | } 192 | } 193 | 194 | if runner.Secret != nil { 195 | _, err = r.UpdateSecret(runner.Name, runner.Secret) 196 | 197 | if err != nil { 198 | return nil, err 199 | } 200 | } 201 | 202 | updatedRunner, err := r.GetRunner(runner.Name) 203 | if err != nil { 204 | return nil, err 205 | } 206 | return updatedRunner, err 207 | } 208 | 209 | func (r *RunnerServiceImpl) DeleteRunner(name string) error { 210 | configMaps, err := helpers.ListConfigMaps(r.config.Kube) 211 | if err != nil { 212 | return err 213 | } 214 | 215 | // Cheching for tasks associated to the runner before deleting it. 216 | for _, configMap := range configMaps.Items { 217 | runnerName := configMap.Data["runner"] 218 | if runnerName == name { 219 | return fmt.Errorf("runner is bound with task: %s , please delete that first", configMap.Data["name"]) 220 | } 221 | } 222 | err = helpers.DeleteConfigMap(r.config.Kube, name) 223 | 224 | if err != nil { 225 | return err 226 | } 227 | 228 | err = helpers.DeleteSecret(r.config.Kube, name) 229 | if err != nil && !errors.IsNotFound(err) { 230 | return err 231 | } 232 | 233 | return nil 234 | } 235 | 236 | func (r *RunnerServiceImpl) GetAdminGroups(secretName string) (string, error) { 237 | secret, err := helpers.GetSecret(r.config.Kube, secretName) 238 | 239 | if err != nil { 240 | return "", err 241 | } 242 | 243 | accessGroups := secret.Data["accessGroups"] 244 | 245 | return string(accessGroups), nil 246 | } 247 | 248 | func (r *RunnerServiceImpl) ListAllJobs() ([]models.Job, error) { 249 | jobs, err := helpers.ListJobs(r.config.Kube, nil) 250 | if err != nil { 251 | return nil, err 252 | } 253 | 254 | var jobsRet []models.Job 255 | for _, job := range jobs.Items { 256 | var jobRet models.Job 257 | jobRet.ID = job.Name 258 | jobRet.Owner = job.Labels["owner"] 259 | jobRet.StartTime = job.Status.StartTime.Format(time.UnixDate) 260 | if job.Status.CompletionTime != nil { 261 | jobRet.CompletionTime = job.Status.CompletionTime.Format(time.UnixDate) 262 | } 263 | jobRet.Failed = job.Status.Failed 264 | jobRet.Completed = job.Status.Succeeded 265 | jobsRet = append(jobsRet, jobRet) 266 | } 267 | 268 | return jobsRet, nil 269 | } 270 | 271 | func (r *RunnerServiceImpl) GetSecret(name string) (map[string]string, error) { 272 | secretCleaned := make(map[string]string) 273 | secret, err := helpers.GetSecret(r.config.Kube, name) 274 | 275 | if err != nil { 276 | return nil, err 277 | } 278 | 279 | for key := range secret.Data { 280 | secretCleaned[key] = "************" 281 | } 282 | return secretCleaned, nil 283 | } 284 | 285 | func (r *RunnerServiceImpl) UpdateSecret(name string, secret map[string]string) (map[string]string, error) { 286 | secretCleaned := make(map[string]string) 287 | secretCurrent := make(map[string]string) 288 | var operation string 289 | 290 | secretObj, err := helpers.GetSecret(r.config.Kube, name) 291 | if err != nil && !errors.IsNotFound(err) { 292 | return nil, err 293 | } 294 | // converting k8s secret from v1.Secret into map[string]string 295 | 296 | if secretObj != nil { 297 | for k, v := range secretObj.Data { 298 | secretCurrent[k] = string(v) 299 | } 300 | 301 | operation = "update" 302 | } else { 303 | operation = "create" 304 | } 305 | 306 | for k, v := range secret { 307 | v2, ok := secretCurrent[k] 308 | 309 | if v != "" && v != v2 { 310 | if v != "************" { 311 | secretCurrent[k] = v 312 | } 313 | } else if v == "" && ok { 314 | delete(secretCurrent, k) 315 | } 316 | } 317 | 318 | if len(secretCurrent) != 0 { 319 | secretNew, err := helpers.CreateOrUpdateSecret(r.config.Kube, name, secretCurrent, operation) 320 | if err != nil { 321 | return secretCleaned, err 322 | } 323 | 324 | for key := range secretNew.Data { 325 | secretCleaned[key] = "************" 326 | } 327 | return secretCleaned, nil 328 | } else { 329 | err := helpers.DeleteSecret(r.config.Kube, name) 330 | if err != nil { 331 | return secretCleaned, err 332 | } 333 | return secretCleaned, nil 334 | } 335 | } 336 | 337 | func (r *RunnerServiceImpl) DeleteSecret(name string) error { 338 | err := helpers.DeleteSecret(r.config.Kube, name) 339 | if err != nil && !errors.IsNotFound(err) { 340 | return err 341 | } 342 | 343 | return nil 344 | } 345 | -------------------------------------------------------------------------------- /services/task_svc.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | "os" 9 | "strconv" 10 | 11 | "github.com/kriten-io/kriten/config" 12 | "github.com/kriten-io/kriten/helpers" 13 | "github.com/kriten-io/kriten/models" 14 | 15 | "github.com/go-openapi/loads" 16 | "github.com/go-openapi/strfmt" 17 | "github.com/go-openapi/validate" 18 | "golang.org/x/exp/slices" 19 | ) 20 | 21 | type TaskService interface { 22 | ListTasks([]string) ([]*models.Task, error) 23 | GetTask(string) (*models.Task, error) 24 | CreateTask(models.Task) (*models.Task, error) 25 | UpdateTask(models.Task) (*models.Task, error) 26 | DeleteTask(string) error 27 | GetSchema(string) (map[string]interface{}, error) 28 | DeleteSchema(string) error 29 | UpdateSchema(string, map[string]interface{}) (map[string]interface{}, error) 30 | } 31 | 32 | type TaskServiceImpl struct { 33 | WebhookService WebhookService 34 | config config.Config 35 | } 36 | 37 | func NewTaskService(ws WebhookService, config config.Config) TaskService { 38 | return &TaskServiceImpl{ 39 | WebhookService: ws, 40 | config: config, 41 | } 42 | } 43 | 44 | func (t *TaskServiceImpl) ListTasks(authList []string) ([]*models.Task, error) { 45 | var tasks []*models.Task 46 | 47 | if len(authList) == 0 { 48 | return tasks, nil 49 | } 50 | 51 | configMaps, err := helpers.ListConfigMaps(t.config.Kube) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | for _, configMap := range configMaps.Items { 57 | runnerName := configMap.Data["runner"] 58 | if runnerName != "" { 59 | if authList[0] == "*" || slices.Contains(authList, configMap.Data["name"]) { 60 | var taskData *models.Task 61 | b, _ := json.Marshal(configMap.Data) 62 | 63 | _ = json.Unmarshal(b, &taskData) 64 | taskData.Synchronous, _ = strconv.ParseBool(configMap.Data["synchronous"]) 65 | if configMap.Data["schema"] != "" { 66 | var jsonData map[string]interface{} 67 | err = json.Unmarshal([]byte(configMap.Data["schema"]), &jsonData) 68 | if err != nil { 69 | return nil, err 70 | } 71 | taskData.Schema = jsonData 72 | } 73 | tasks = append(tasks, taskData) 74 | } 75 | } 76 | } 77 | 78 | return tasks, nil 79 | } 80 | 81 | func (t *TaskServiceImpl) GetTask(name string) (*models.Task, error) { 82 | var taskData models.Task 83 | configMap, err := helpers.GetConfigMap(t.config.Kube, name) 84 | if err != nil { 85 | return nil, err 86 | } 87 | if configMap.Data["runner"] == "" { 88 | return nil, fmt.Errorf("task %s not found", name) 89 | } 90 | 91 | // TODO: this is a temporary solution to return synchronous as a boolean 92 | b, _ := json.Marshal(configMap.Data) 93 | 94 | _ = json.Unmarshal(b, &taskData) 95 | taskData.Synchronous, _ = strconv.ParseBool(configMap.Data["synchronous"]) 96 | 97 | if configMap.Data["schema"] != "" { 98 | var jsonData map[string]interface{} 99 | err = json.Unmarshal([]byte(configMap.Data["schema"]), &jsonData) 100 | if err != nil { 101 | return nil, err 102 | } 103 | taskData.Schema = jsonData 104 | } 105 | 106 | return &taskData, nil 107 | } 108 | 109 | func (t *TaskServiceImpl) CreateTask(task models.Task) (*models.Task, error) { 110 | var jsonData []byte 111 | err := helpers.ValidateK8sConfigMapName(task.Name) 112 | if err != nil { 113 | return nil, fmt.Errorf("%w", err) 114 | } 115 | runner, err := helpers.GetConfigMap(t.config.Kube, task.Runner) 116 | if err != nil || runner.Data["image"] == "" { 117 | return nil, fmt.Errorf("error retrieving runner %s, please specify an existing runner", task.Runner) 118 | } 119 | 120 | if task.Schema != nil { 121 | jsonData, err = json.Marshal(task.Schema) 122 | if err != nil { 123 | return nil, err 124 | } 125 | 126 | err = ValidateSchema(jsonData) 127 | if err != nil { 128 | return nil, err 129 | } 130 | } 131 | 132 | // Parsing a models.Task into a map 133 | b, _ := json.Marshal(task) 134 | var data map[string]string 135 | _ = json.Unmarshal(b, &data) 136 | data["synchronous"] = strconv.FormatBool(task.Synchronous) 137 | data["schema"] = string(jsonData) 138 | delete(data, "secret") 139 | 140 | _, err = helpers.CreateOrUpdateConfigMap(t.config.Kube, data, "create") 141 | if err != nil { 142 | return nil, err 143 | } 144 | 145 | configuredTask, err := t.GetTask(task.Name) 146 | if err != nil { 147 | return nil, err 148 | } 149 | return configuredTask, err 150 | } 151 | 152 | func (t *TaskServiceImpl) UpdateTask(task models.Task) (*models.Task, error) { 153 | var jsonData []byte 154 | 155 | _, err := helpers.GetConfigMap(t.config.Kube, task.Name) 156 | if err != nil { 157 | return nil, err 158 | } 159 | 160 | runner, err := helpers.GetConfigMap(t.config.Kube, task.Runner) 161 | if err != nil || runner.Data["image"] == "" { 162 | return nil, fmt.Errorf("error retrieving runner %s, please specify an existing runner", task.Runner) 163 | } 164 | 165 | if task.Schema != nil { 166 | jsonData, err = json.Marshal(task.Schema) 167 | if err != nil { 168 | return nil, err 169 | } 170 | 171 | err = ValidateSchema(jsonData) 172 | if err != nil { 173 | return nil, err 174 | } 175 | } 176 | 177 | // Parsing a models.Task into a map 178 | b, _ := json.Marshal(task) 179 | var data map[string]string 180 | _ = json.Unmarshal(b, &data) 181 | data["synchronous"] = strconv.FormatBool(task.Synchronous) 182 | data["schema"] = string(jsonData) 183 | 184 | _, err = helpers.CreateOrUpdateConfigMap(t.config.Kube, data, "update") 185 | if err != nil { 186 | return nil, err 187 | } 188 | 189 | configuredTask, err := t.GetTask(task.Name) 190 | if err != nil { 191 | return nil, err 192 | } 193 | return configuredTask, err 194 | } 195 | 196 | func (t *TaskServiceImpl) DeleteTask(name string) error { 197 | res, err := t.WebhookService.ListTaskWebhooks(name) 198 | if len(res) != 0 { 199 | return fmt.Errorf("cannot delete task %s, please remove associated webhooks first", name) 200 | } 201 | 202 | err = helpers.DeleteConfigMap(t.config.Kube, name) 203 | if err != nil { 204 | return err 205 | } 206 | 207 | return nil 208 | } 209 | 210 | func (t *TaskServiceImpl) GetSchema(name string) (map[string]interface{}, error) { 211 | var data map[string]any 212 | 213 | configMap, err := helpers.GetConfigMap(t.config.Kube, name) 214 | if err != nil { 215 | return nil, err 216 | } 217 | if configMap.Data["runner"] == "" { 218 | return nil, fmt.Errorf("task %s not found", name) 219 | } 220 | 221 | if configMap.Data["schema"] != "" { 222 | err = json.Unmarshal([]byte(configMap.Data["schema"]), &data) 223 | if err != nil { 224 | return nil, err 225 | } 226 | } 227 | 228 | return data, nil 229 | } 230 | 231 | func (t *TaskServiceImpl) UpdateSchema(taskName string, schema map[string]interface{}) (map[string]interface{}, error) { 232 | task, err := helpers.GetConfigMap(t.config.Kube, taskName) 233 | if err != nil { 234 | return nil, err 235 | } 236 | if task.Data["runner"] == "" { 237 | return nil, fmt.Errorf("task %s not found", taskName) 238 | } 239 | 240 | data, err := json.Marshal(schema) 241 | if err != nil { 242 | return nil, err 243 | } 244 | 245 | err = ValidateSchema(data) 246 | if err != nil { 247 | return nil, err 248 | } 249 | 250 | task.Data["schema"] = string(data) 251 | _, err = helpers.CreateOrUpdateConfigMap(t.config.Kube, task.Data, "update") 252 | if err != nil { 253 | return nil, err 254 | } 255 | 256 | return schema, nil 257 | } 258 | 259 | func (t *TaskServiceImpl) DeleteSchema(name string) error { 260 | task, err := t.GetTask(name) 261 | if err != nil { 262 | return err 263 | } 264 | 265 | if task.Schema == nil { 266 | return nil 267 | } 268 | 269 | // Parsing a models.Task into a map 270 | b, _ := json.Marshal(task) 271 | var data map[string]string 272 | _ = json.Unmarshal(b, &data) 273 | delete(data, "schema") 274 | _, err = helpers.CreateOrUpdateConfigMap(t.config.Kube, data, "update") 275 | if err != nil { 276 | return err 277 | } 278 | 279 | return nil 280 | } 281 | 282 | func ValidateSchema(schema []byte) error { 283 | input, err := os.ReadFile("spec.json") 284 | if err != nil { 285 | log.Println(err) 286 | return err 287 | } 288 | 289 | output := bytes.ReplaceAll(input, []byte("\"%schema%\""), schema) 290 | doc, err := loads.Analyzed(output, "2.0") 291 | if err != nil { 292 | log.Printf("error while loading spec: %v\n", err) 293 | return err 294 | } 295 | 296 | validate.SetContinueOnErrors(true) // Set global options 297 | err = validate.Spec(doc, strfmt.Default) // Validates spec with default Swagger 2.0 format definitions 298 | 299 | if err != nil { 300 | log.Printf("This spec has some validation errors: %v\n", err) 301 | return err 302 | } 303 | 304 | return nil 305 | } 306 | -------------------------------------------------------------------------------- /services/tokens_svc.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "crypto/rand" 5 | "fmt" 6 | "math/big" 7 | "time" 8 | 9 | "github.com/kriten-io/kriten/config" 10 | "github.com/kriten-io/kriten/helpers" 11 | "github.com/kriten-io/kriten/models" 12 | 13 | uuid "github.com/satori/go.uuid" 14 | "golang.org/x/exp/slices" 15 | 16 | "gorm.io/gorm" 17 | ) 18 | 19 | type ApiTokenService interface { 20 | ListApiTokens(uuid.UUID) ([]models.ApiToken, error) 21 | ListAllApiTokens([]string) ([]models.ApiToken, error) 22 | GetApiToken(string) (models.ApiToken, error) 23 | CreateApiToken(models.ApiToken) (models.ApiToken, error) 24 | UpdateApiToken(models.ApiToken) (models.ApiToken, error) 25 | DeleteApiToken(string) error 26 | } 27 | 28 | type ApiTokenServiceImpl struct { 29 | db *gorm.DB 30 | config config.Config 31 | } 32 | 33 | func NewApiTokenService(database *gorm.DB, config config.Config) ApiTokenService { 34 | return &ApiTokenServiceImpl{ 35 | db: database, 36 | config: config, 37 | } 38 | } 39 | 40 | func (u *ApiTokenServiceImpl) ListApiTokens(userid uuid.UUID) ([]models.ApiToken, error) { 41 | var apiTokens []models.ApiToken 42 | 43 | res := u.db.Select("id", "owner", "expires", "created_at", "updated_at", "description", "enabled"). 44 | Where("owner = ?", userid). 45 | Find(&apiTokens) 46 | 47 | if res.Error != nil { 48 | return apiTokens, res.Error 49 | } 50 | 51 | return apiTokens, nil 52 | } 53 | 54 | func (u *ApiTokenServiceImpl) ListAllApiTokens(authList []string) ([]models.ApiToken, error) { 55 | var apiTokens []models.ApiToken 56 | var res *gorm.DB 57 | 58 | if len(authList) == 0 { 59 | return apiTokens, nil 60 | } 61 | 62 | if slices.Contains(authList, "*") { 63 | res = u.db.Find(&apiTokens) 64 | } else { 65 | res = u.db.Find(&apiTokens, authList) 66 | } 67 | if res.Error != nil { 68 | return apiTokens, res.Error 69 | } 70 | 71 | return apiTokens, nil 72 | } 73 | 74 | func (u *ApiTokenServiceImpl) GetApiToken(id string) (models.ApiToken, error) { 75 | var apiToken models.ApiToken 76 | 77 | res := u.db.Select("id", "owner", "expires", "created_at", "updated_at", "description", "enabled"). 78 | Where("id = ?", id). 79 | Find(&apiToken) 80 | 81 | if res.Error != nil { 82 | return models.ApiToken{}, res.Error 83 | } 84 | 85 | if res.RowsAffected == 0 { 86 | return models.ApiToken{}, fmt.Errorf("token %s not found, please check uuid", id) 87 | } 88 | 89 | return apiToken, nil 90 | } 91 | 92 | func (u *ApiTokenServiceImpl) CreateApiToken(apiToken models.ApiToken) (models.ApiToken, error) { 93 | key, err := GenerateToken(40) 94 | if err != nil { 95 | return models.ApiToken{}, err 96 | } 97 | var tokenEnabled = true 98 | apiToken.Key = helpers.GenerateHMAC(u.config.APISecret, key) 99 | 100 | // if No value is passed, initialise to Zero value 101 | if apiToken.Expires == nil { 102 | apiToken.Expires = new(time.Time) 103 | } 104 | 105 | // nil pointer dereference via tokenEnabled bool var 106 | if apiToken.Enabled == nil { 107 | apiToken.Enabled = &tokenEnabled 108 | } 109 | 110 | res := u.db.Create(&apiToken) 111 | 112 | // Passing unencripted key on creation 113 | apiToken.Key = key 114 | 115 | return apiToken, res.Error 116 | } 117 | 118 | func (u *ApiTokenServiceImpl) UpdateApiToken(apiToken models.ApiToken) (models.ApiToken, error) { 119 | oldToken, err := u.GetApiToken(apiToken.ID.String()) 120 | if err != nil { 121 | return models.ApiToken{}, err 122 | } 123 | 124 | if apiToken.Enabled != nil { 125 | oldToken.Enabled = apiToken.Enabled 126 | } 127 | if apiToken.Description != "" { 128 | oldToken.Description = apiToken.Description 129 | } 130 | if apiToken.Expires != nil { 131 | oldToken.Expires = apiToken.Expires 132 | } 133 | 134 | res := u.db.Updates(oldToken) 135 | if res.Error != nil { 136 | return models.ApiToken{}, res.Error 137 | } 138 | 139 | newToken, err := u.GetApiToken(apiToken.ID.String()) 140 | if err != nil { 141 | return models.ApiToken{}, err 142 | } 143 | return newToken, nil 144 | } 145 | 146 | func (u *ApiTokenServiceImpl) DeleteApiToken(id string) error { 147 | apiToken, err := u.GetApiToken(id) 148 | if err != nil { 149 | return err 150 | } 151 | return u.db.Unscoped().Delete(&apiToken).Error 152 | } 153 | 154 | func GenerateToken(n int) (string, error) { 155 | // Removing 4 chars from the total length for "kri_" prefix 156 | n -= 4 157 | const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" 158 | ret := make([]byte, n) 159 | for i := 0; i < n; i++ { 160 | num, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters)))) 161 | if err != nil { 162 | return "", err 163 | } 164 | ret[i] = letters[num.Int64()] 165 | } 166 | 167 | return "kri_" + string(ret), nil 168 | } 169 | -------------------------------------------------------------------------------- /services/users_svc.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/kriten-io/kriten/config" 7 | "github.com/kriten-io/kriten/models" 8 | 9 | "golang.org/x/exp/slices" 10 | 11 | "github.com/go-errors/errors" 12 | "github.com/lib/pq" 13 | "golang.org/x/crypto/bcrypt" 14 | "gorm.io/gorm" 15 | ) 16 | 17 | type UserService interface { 18 | ListUsers([]string) ([]models.User, error) 19 | GetUser(string) (models.User, error) 20 | CreateUser(models.User) (models.User, error) 21 | UpdateUser(models.User) (models.User, error) 22 | DeleteUser(string) error 23 | GetByUsernameAndProvider(string, string) (models.User, error) 24 | AddGroup(models.User, string) (models.User, error) 25 | RemoveGroup(models.User, string) (models.User, error) 26 | GetUserRoles(string, string) ([]models.Role, error) 27 | } 28 | 29 | type UserServiceImpl struct { 30 | db *gorm.DB 31 | config config.Config 32 | } 33 | 34 | func NewUserService(database *gorm.DB, config config.Config) UserService { 35 | return &UserServiceImpl{ 36 | db: database, 37 | config: config, 38 | } 39 | } 40 | 41 | func (u *UserServiceImpl) ListUsers(authList []string) ([]models.User, error) { 42 | var users []models.User 43 | var res *gorm.DB 44 | 45 | if len(authList) == 0 { 46 | return users, nil 47 | } 48 | 49 | if slices.Contains(authList, "*") { 50 | res = u.db.Find(&users) 51 | } else { 52 | res = u.db.Find(&users, authList) 53 | } 54 | if res.Error != nil { 55 | return users, res.Error 56 | } 57 | 58 | return users, nil 59 | } 60 | 61 | func (u *UserServiceImpl) GetUser(id string) (models.User, error) { 62 | var user models.User 63 | res := u.db.Where("user_id = ?", id).Find(&user) 64 | if res.Error != nil { 65 | return models.User{}, res.Error 66 | } 67 | 68 | if user.Username == "" { 69 | return models.User{}, fmt.Errorf("user %s not found, please check uuid", id) 70 | } 71 | 72 | return user, nil 73 | } 74 | 75 | func (u *UserServiceImpl) CreateUser(user models.User) (models.User, error) { 76 | if user.Provider == "local" { 77 | password, err := HashPassword(user.Password) 78 | if err != nil { 79 | return models.User{}, err 80 | } 81 | user.Password = password 82 | } 83 | 84 | res := u.db.Create(&user) 85 | 86 | return user, res.Error 87 | } 88 | 89 | func (u *UserServiceImpl) UpdateUser(user models.User) (models.User, error) { 90 | password, err := HashPassword(user.Password) 91 | if err != nil { 92 | return models.User{}, err 93 | } 94 | 95 | user.Password = password 96 | res := u.db.Updates(user) 97 | if res.Error != nil { 98 | return models.User{}, res.Error 99 | } 100 | 101 | newUser, err := u.GetUser(user.ID.String()) 102 | if err != nil { 103 | return models.User{}, err 104 | } 105 | return newUser, nil 106 | } 107 | 108 | func (u *UserServiceImpl) DeleteUser(id string) error { 109 | user, err := u.GetUser(id) 110 | if err != nil { 111 | return err 112 | } 113 | if len(user.Groups) != 0 { 114 | return errors.New("cannot delete user who is part of a group") 115 | } 116 | 117 | var apiTokens []models.ApiToken 118 | res := u.db.Where("owner = ?", id).Find(&apiTokens) 119 | 120 | if res.Error != nil { 121 | return res.Error 122 | } 123 | if res.RowsAffected != 0 { 124 | return errors.New("found API tokens associated to the user, please delete those first") 125 | } 126 | 127 | return u.db.Unscoped().Delete(&user).Error 128 | } 129 | 130 | func (u *UserServiceImpl) GetByUsernameAndProvider(username string, provider string) (models.User, error) { 131 | var user models.User 132 | res := u.db.Where("username = ? AND provider = ?", username, provider).Find(&user) 133 | if res.Error != nil { 134 | return models.User{}, res.Error 135 | } 136 | 137 | if user.Username == "" { 138 | return models.User{}, errors.New("user not found") 139 | } 140 | 141 | return user, nil 142 | } 143 | 144 | func (u *UserServiceImpl) AddGroup(user models.User, newGroup string) (models.User, error) { 145 | if user.Groups == nil || len(user.Groups) == 0 { 146 | user.Groups = pq.StringArray{newGroup} 147 | } else if !slices.Contains(user.Groups, newGroup) { 148 | user.Groups = append(user.Groups, newGroup) 149 | } 150 | 151 | res := u.db.Updates(user) 152 | if res.Error != nil { 153 | return models.User{}, res.Error 154 | } 155 | 156 | return user, nil 157 | } 158 | 159 | func (u *UserServiceImpl) RemoveGroup(user models.User, group string) (models.User, error) { 160 | found := false 161 | 162 | for key, value := range user.Groups { 163 | if value == group { 164 | user.Groups = append(user.Groups[:key], user.Groups[key+1:]...) 165 | found = true 166 | break 167 | } 168 | } 169 | 170 | if found { 171 | res := u.db.Updates(user) 172 | if res.Error != nil { 173 | return models.User{}, res.Error 174 | } 175 | } 176 | 177 | return user, nil 178 | } 179 | 180 | func (u *UserServiceImpl) GetUserRoles(userID string, provider string) ([]models.Role, error) { 181 | var roles []models.Role 182 | var groups []string 183 | 184 | user, err := u.GetUser(userID) 185 | if err != nil { 186 | return nil, err 187 | } 188 | groups = user.Groups 189 | // SELECT * 190 | // FROM roles 191 | // INNER JOIN role_bindings 192 | // ON roles.role_id = role_bindings.role_id 193 | // WHERE role_bindings.subject_provider = provider AND role_bindings.subject_id = subjectID; 194 | res := u.db.Model(&models.Role{}).Joins( 195 | "left join role_bindings on roles.role_id = role_bindings.role_id").Where( 196 | "role_bindings.subject_provider = ? AND role_bindings.subject_id IN ?", provider, groups).Find(&roles) 197 | if res.Error != nil { 198 | return []models.Role{}, res.Error 199 | } 200 | return roles, nil 201 | } 202 | 203 | func HashPassword(password string) (string, error) { 204 | bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) 205 | return string(bytes), err 206 | } 207 | -------------------------------------------------------------------------------- /services/webhook_svc.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/kriten-io/kriten/config" 7 | "github.com/kriten-io/kriten/models" 8 | 9 | "gorm.io/gorm" 10 | 11 | uuid "github.com/satori/go.uuid" 12 | "golang.org/x/exp/slices" 13 | ) 14 | 15 | type WebhookService interface { 16 | ListWebhooks(uuid.UUID) ([]models.Webhook, error) 17 | ListTaskWebhooks(string) ([]models.Webhook, error) 18 | ListAllWebhooks([]string) ([]models.Webhook, error) 19 | GetWebhook(string) (models.Webhook, error) 20 | CreateWebhook(models.Webhook) (models.Webhook, error) 21 | DeleteWebhook(string) error 22 | } 23 | 24 | type WebhookServiceImpl struct { 25 | db *gorm.DB 26 | config config.Config 27 | } 28 | 29 | func NewWebhookService(database *gorm.DB, config config.Config) WebhookService { 30 | return &WebhookServiceImpl{ 31 | db: database, 32 | config: config, 33 | } 34 | } 35 | 36 | func (w *WebhookServiceImpl) ListWebhooks(userid uuid.UUID) ([]models.Webhook, error) { 37 | var webHooks []models.Webhook 38 | 39 | res := w.db.Select("id", "owner", "secret", "description", "task", "created_at", "updated_at"). 40 | Where("owner = ?", userid). 41 | Find(&webHooks) 42 | 43 | if res.Error != nil { 44 | return webHooks, res.Error 45 | } 46 | 47 | return webHooks, nil 48 | } 49 | 50 | func (w *WebhookServiceImpl) ListTaskWebhooks(taskName string) ([]models.Webhook, error) { 51 | var webHooks []models.Webhook 52 | 53 | res := w.db.Select("id", "owner", "secret", "description", "task", "created_at", "updated_at"). 54 | Where("task = ?", taskName). 55 | Find(&webHooks) 56 | 57 | if res.Error != nil { 58 | return webHooks, res.Error 59 | } 60 | 61 | return webHooks, nil 62 | } 63 | 64 | func (w *WebhookServiceImpl) ListAllWebhooks(authList []string) ([]models.Webhook, error) { 65 | var webHooks []models.Webhook 66 | var res *gorm.DB 67 | 68 | if len(authList) == 0 { 69 | return webHooks, nil 70 | } 71 | 72 | if slices.Contains(authList, "*") { 73 | res = w.db.Find(&webHooks) 74 | } else { 75 | res = w.db.Find(&webHooks, authList) 76 | } 77 | if res.Error != nil { 78 | return webHooks, res.Error 79 | } 80 | 81 | return webHooks, nil 82 | } 83 | 84 | func (w *WebhookServiceImpl) GetWebhook(id string) (models.Webhook, error) { 85 | var webHook models.Webhook 86 | 87 | res := w.db.Select("id", "owner", "secret", "description", "task", "created_at", "updated_at"). 88 | Where("id = ?", id). 89 | Find(&webHook) 90 | 91 | if res.Error != nil { 92 | return models.Webhook{}, res.Error 93 | } 94 | 95 | if res.RowsAffected == 0 { 96 | return models.Webhook{}, fmt.Errorf("webhook %s not found, please check uuid", id) 97 | } 98 | 99 | return webHook, nil 100 | } 101 | 102 | func (w *WebhookServiceImpl) CreateWebhook(webHook models.Webhook) (models.Webhook, error) { 103 | res := w.db.Create(&webHook) 104 | 105 | return webHook, res.Error 106 | } 107 | 108 | func (w *WebhookServiceImpl) DeleteWebhook(id string) error { 109 | webHook, err := w.GetWebhook(id) 110 | if err != nil { 111 | return err 112 | } 113 | return w.db.Unscoped().Delete(&webHook).Error 114 | } 115 | -------------------------------------------------------------------------------- /spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "version": "1.0.0", 5 | "title": "Open API template" 6 | }, 7 | "paths": {}, 8 | "definitions": { 9 | "Schema": "%schema%" 10 | } 11 | } 12 | --------------------------------------------------------------------------------