├── .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 |
3 |
4 | Kriten
5 |
6 |
7 | Visit kriten.io for the full documentation, examples and guides.
8 |
9 |
10 |
11 |
12 | [](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 |
--------------------------------------------------------------------------------