Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Idea: Improve Feature Access #792

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions api/handle_feature.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package api

import (
"net/http"

"github.com/checkmarble/marble-backend/dto"
"github.com/checkmarble/marble-backend/models"
"github.com/checkmarble/marble-backend/pure_utils"
"github.com/checkmarble/marble-backend/usecases"
"github.com/checkmarble/marble-backend/utils"
"github.com/gin-gonic/gin"
)

func handleListFeatures(uc usecases.Usecases) func(c *gin.Context) {
return func(c *gin.Context) {
ctx := c.Request.Context()
usecase := usecasesWithCreds(ctx, uc).NewFeatureUseCase()
features, err := usecase.ListAllFeatures(ctx)

if presentError(ctx, c, err) {
return
}
c.JSON(http.StatusOK, gin.H{"features": pure_utils.Map(features, dto.AdaptFeatureDto)})
}
}

func handleCreateFeature(uc usecases.Usecases) func(c *gin.Context) {
return func(c *gin.Context) {
ctx := c.Request.Context()
var data dto.CreateFeatureBody
if err := c.ShouldBindJSON(&data); err != nil {
c.Status(http.StatusBadRequest)
return
}

usecase := usecasesWithCreds(ctx, uc).NewFeatureUseCase()
feature, err := usecase.CreateFeature(ctx, models.CreateFeatureAttributes{
Name: data.Name,
})

if presentError(ctx, c, err) {
return
}
c.JSON(http.StatusCreated, gin.H{"feature": dto.AdaptFeatureDto(feature)})
}
}

type FeatureUriInput struct {
FeatureId string `uri:"feature_id" binding:"required,uuid"`
}

func handleGetFeature(uc usecases.Usecases) func(c *gin.Context) {
return func(c *gin.Context) {
ctx := c.Request.Context()
var featureInput FeatureUriInput
if err := c.ShouldBindUri(&featureInput); err != nil {
c.Status(http.StatusBadRequest)
return
}

usecase := usecasesWithCreds(ctx, uc).NewFeatureUseCase()
feature, err := usecase.GetFeatureById(ctx, featureInput.FeatureId)

if presentError(ctx, c, err) {
return
}
c.JSON(http.StatusOK, gin.H{"feature": dto.AdaptFeatureDto(feature)})
}
}

func handleUpdateFeature(uc usecases.Usecases) func(c *gin.Context) {
return func(c *gin.Context) {
ctx := c.Request.Context()
var featureInput FeatureUriInput
if err := c.ShouldBindUri(&featureInput); err != nil {
c.Status(http.StatusBadRequest)
return
}

var data dto.UpdateFeatureBody
if err := c.ShouldBindJSON(&data); err != nil {
c.Status(http.StatusBadRequest)
return
}

usecase := usecasesWithCreds(ctx, uc).NewFeatureUseCase()
feature, err := usecase.UpdateFeature(ctx, models.UpdateFeatureAttributes{
Name: data.Name,
})

if presentError(ctx, c, err) {
return
}
c.JSON(http.StatusOK, gin.H{"feature": dto.AdaptFeatureDto(feature)})
}
}

func handleDeleteFeature(uc usecases.Usecases) func(c *gin.Context) {
return func(c *gin.Context) {
ctx := c.Request.Context()
organizationId, err := utils.OrganizationIdFromRequest(c.Request)
if presentError(ctx, c, err) {
return
}

var featureInput FeatureUriInput
if err := c.ShouldBindUri(&featureInput); err != nil {
c.Status(http.StatusBadRequest)
return
}

usecase := usecasesWithCreds(ctx, uc).NewFeatureUseCase()
err = usecase.DeleteFeature(ctx, organizationId, featureInput.FeatureId)

if presentError(ctx, c, err) {
return
}
c.Status(http.StatusNoContent)
}
}
17 changes: 17 additions & 0 deletions api/handle_license.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package api

import (
"net/http"
"os"
"strings"

"github.com/gin-gonic/gin"
Expand Down Expand Up @@ -98,3 +99,19 @@ func handleValidateLicense(uc usecases.Usecases) func(c *gin.Context) {
c.JSON(http.StatusOK, dto.AdaptLicenseValidationDto(licenseValidation))
}
}

func handleIsSSOEnabled(uc usecases.Usecases) func(c *gin.Context) {
return func(c *gin.Context) {
ctx := c.Request.Context()
licenseKey := os.Getenv("LICENSE_KEY")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way the endpoint works is broadly ok, but I'm trying to stick as much as possible to the paradigm of "all env variables are read in the entrypoint file for the code that is run" - here, typically cmd/server.go. Any useful pre-treatment is then done and the useful value is injected to the usecases from there. See cmd/server.go:100.
The idea being that I want to avoid a situation where we start reading env variables right and left, with business logic depending on it. (and we keep open the possibility to easily switch to a config file setup option, with just one file to change).

Further, and also check cmd/server.go:100 on this, unfortunately the license key is a bit of a special case that further justifies this. Our backend also doubles as the license server (I promise I'll change this some day and run a distinct container. But for now we have to bear with it). Anyway, TL:DR our own backend should not call itself for the license (or else, all kinds of chicken-and-egg problems), so it's just validating a whitelist of GCP project ids from the metadata server.


usecase := uc.NewLicenseUsecase()
licenseValidation, err := usecase.ValidateLicense(ctx, strings.TrimPrefix(licenseKey, "/"))
if presentError(ctx, c, err) {
return
}
c.JSON(http.StatusOK, gin.H{
"is_sso_enabled": licenseValidation.Sso,
})
}
}
40 changes: 40 additions & 0 deletions api/handle_organization.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,43 @@ func handleDeleteOrganization(uc usecases.Usecases) func(c *gin.Context) {
c.Status(http.StatusNoContent)
}
}

func handleGetOrganizationEntitlements(uc usecases.Usecases) func(c *gin.Context) {
return func(c *gin.Context) {
ctx := c.Request.Context()
organizationID := c.Param("organization_id")

usecase := usecasesWithCreds(ctx, uc).NewOrganizationUseCase()
entitlements, err := usecase.GetOrganizationEntitlements(ctx, organizationID)
if presentError(ctx, c, err) {
return
}

c.JSON(http.StatusOK, gin.H{
"entitlements": pure_utils.Map(entitlements, dto.AdaptOrganizationEntitlementDto),
})
}
}

func handleUpdateOrganizationEntitlements(uc usecases.Usecases) func(c *gin.Context) {
return func(c *gin.Context) {
ctx := c.Request.Context()
organizationID := c.Param("organization_id")
var data dto.UpdateOrganizationEntitlementBodyDto
if err := c.ShouldBindJSON(&data); err != nil {
c.Status(http.StatusBadRequest)
return
}

usecase := usecasesWithCreds(ctx, uc).NewOrganizationUseCase()
err := usecase.UpdateOrganizationEntitlements(ctx, organizationID, models.UpdateOrganizationEntitlementInput{
FeatureId: data.FeatureId,
Availability: models.FeatureAvailabilityFrom(data.Availability),
OrganizationId: organizationID,
})
if presentError(ctx, c, err) {
return
}
c.Status(http.StatusNoContent)
}
}
10 changes: 10 additions & 0 deletions api/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ func addRoutes(r *gin.Engine, conf Configuration, uc usecases.Usecases, auth Aut
r.GET("/liveness", tom, handleLivenessProbe(uc))
r.POST("/token", tom, tokenHandler.GenerateToken)
r.GET("/validate-license/*license_key", tom, handleValidateLicense(uc))
r.GET("/is-sso-enabled", tom, handleIsSSOEnabled(uc))

router := r.Use(auth.Middleware)

Expand Down Expand Up @@ -128,6 +129,9 @@ func addRoutes(r *gin.Engine, conf Configuration, uc usecases.Usecases, auth Aut
router.GET("/organizations/:organization_id", tom, handleGetOrganization(uc))
router.PATCH("/organizations/:organization_id", tom, handlePatchOrganization(uc))
router.DELETE("/organizations/:organization_id", tom, handleDeleteOrganization(uc))
router.GET("/organizations/:organization_id/entitlements", tom, handleGetOrganizationEntitlements(uc))
router.PATCH("/organizations/:organization_id/entitlements", tom,
handleUpdateOrganizationEntitlements(uc))

router.GET("/partners", tom, handleListPartners(uc))
router.POST("/partners", tom, handleCreatePartner(uc))
Expand Down Expand Up @@ -202,4 +206,10 @@ func addRoutes(r *gin.Engine, conf Configuration, uc usecases.Usecases, auth Aut
router.DELETE("/webhooks/:webhook_id", tom, handleDeleteWebhook(uc))

router.GET("/rule-snoozes/:rule_snooze_id", tom, handleGetSnoozesById(uc))

router.GET("/features", tom, handleListFeatures(uc))
router.GET("/features/:feature_id", tom, handleGetFeature(uc))
router.POST("/features", tom, handleCreateFeature(uc))
router.PATCH("/features/:feature_id", tom, handleUpdateFeature(uc))
router.DELETE("/features/:feature_id", tom, handleDeleteFeature(uc))
}
5 changes: 3 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
services:
db:
container_name: postgres
image: europe-west1-docker.pkg.dev/marble-infra/marble/postgresql-db:latest # custom image of postgres 15 with pg_cron extension added
#image: europe-west1-docker.pkg.dev/marble-infra/marble/postgresql-db:latest # custom image of postgres 15 with pg_cron extension added
image: postgres:15.2-alpine
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just heads up to roll this back before merging

shm_size: 1g
restart: always
environment:
Expand All @@ -14,7 +15,7 @@ services:
volumes:
- postgres-db:/data/postgres
healthcheck:
test: [ "CMD-SHELL", "pg_isready -U postgres" ]
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 2s
timeout: 1s
retries: 5
Expand Down
29 changes: 29 additions & 0 deletions dto/feature_dto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package dto

import (
"time"

"github.com/checkmarble/marble-backend/models"
)

type APIFeature struct {
Id string `json:"id"`
Name string `json:"name"`
CreatedAt time.Time `json:"created_at"`
}

func AdaptFeatureDto(f models.Feature) APIFeature {
return APIFeature{
Id: f.Id,
Name: f.Name,
CreatedAt: f.CreatedAt,
}
}

type CreateFeatureBody struct {
Name string `json:"name" binding:"required"`
}

type UpdateFeatureBody struct {
Name string `json:"name"`
}
32 changes: 32 additions & 0 deletions dto/organization_entitlement_dto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package dto

import (
"time"

"github.com/checkmarble/marble-backend/models"
)

type APIOrganizationEntitlement struct {
Id string `json:"id"`
OrganizationId string `json:"organization_id"`
FeatureId string `json:"feature_id"`
Availability string `json:"availability"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

func AdaptOrganizationEntitlementDto(f models.OrganizationEntitlement) APIOrganizationEntitlement {
return APIOrganizationEntitlement{
Id: f.Id,
OrganizationId: f.OrganizationId,
FeatureId: f.FeatureId,
Availability: f.Availability.String(),
CreatedAt: f.CreatedAt,
UpdatedAt: f.UpdatedAt,
}
}

type UpdateOrganizationEntitlementBodyDto struct {
FeatureId string `json:"feature_id" binding:"required"`
Availability string `json:"availability" binding:"required"`
}
3 changes: 3 additions & 0 deletions models/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ const (
AnalyticsTagCreated AnalyticsEvent = "Created a Tag"
AnalyticsTagUpdated AnalyticsEvent = "Updated a Tag"
AnalyticsTagDeleted AnalyticsEvent = "Deleted a Tag"
AnalyticsFeatureCreated AnalyticsEvent = "Created a Feature"
AnalyticsFeatureUpdated AnalyticsEvent = "Updated a Feature"
AnalyticsFeatureDeleted AnalyticsEvent = "Deleted a Feature"
AnalyticsUserCreated AnalyticsEvent = "Created a User"
AnalyticsUserUpdated AnalyticsEvent = "Updated a User"
AnalyticsUserDeleted AnalyticsEvent = "Deleted a User"
Expand Down
30 changes: 30 additions & 0 deletions models/feature.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package models

import "time"

type Feature struct {
Id string
Name string
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time
}

type CreateFeatureInput struct {
Id string
Name string
}

type CreateFeatureAttributes struct {
Name string
}

type UpdateFeatureInput struct {
Id string
Name string
}

type UpdateFeatureAttributes struct {
Id string
Name string
}
38 changes: 38 additions & 0 deletions models/feature_availability.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package models

type FeatureAvailability int

const (
Enable FeatureAvailability = iota
Disable
Test
UnknownFeatureAvailability
)

var ValidFeaturesAvailability = []FeatureAvailability{Enable, Disable, Test}

// Provide a string value for each outcome
func (f FeatureAvailability) String() string {
switch f {
case Enable:
return "enable"
case Disable:
return "disable"
case Test:
return "test"
}
return "unknown"
}

// Provide an Outcome from a string value
func FeatureAvailabilityFrom(s string) FeatureAvailability {
switch s {
case "enable":
return Enable
case "disable":
return Disable
case "test":
return Test
}
return UnknownFeatureAvailability
}
Loading
Loading