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

Adding organization_feature_access table & functionality #796

Merged
merged 8 commits into from
Jan 13, 2025
Merged
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
9 changes: 9 additions & 0 deletions api/handle_license.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,12 @@ 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) {
usecase := uc.NewLicenseUsecase()
c.JSON(http.StatusOK, gin.H{
"is_sso_enabled": usecase.HasSsoEnabled(),
})
}
}
37 changes: 37 additions & 0 deletions api/handle_organization.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,40 @@ func handleDeleteOrganization(uc usecases.Usecases) func(c *gin.Context) {
c.Status(http.StatusNoContent)
}
}

func handleGetOrganizationFeatureAccess(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()
featureAccess, err := usecase.GetOrganizationFeatureAccess(ctx, organizationID)
if presentError(ctx, c, err) {
return
}
c.JSON(http.StatusOK, gin.H{
"feature_access": dto.AdaptOrganizationFeatureAccessDto(featureAccess),
})
}
}

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

usecase := usecasesWithCreds(ctx, uc).NewOrganizationUseCase()
err := usecase.UpdateOrganizationFeatureAccess(ctx,
dto.AdaptUpdateOrganizationFeatureAccessInput(data, organizationID))
if presentError(ctx, c, err) {
return
}

c.Status(http.StatusNoContent)
}
}
4 changes: 4 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-available", tom, handleIsSSOEnabled(uc))

router := r.Use(auth.Middleware)

Expand Down Expand Up @@ -129,6 +130,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/feature_access", tom, handleGetOrganizationFeatureAccess(uc))
router.PATCH("/organizations/:organization_id/feature_access", tom,
carere marked this conversation as resolved.
Show resolved Hide resolved
handlePatchOrganizationFeatureAccess(uc))

router.GET("/partners", tom, handleListPartners(uc))
router.POST("/partners", tom, handleCreatePartner(uc))
Expand Down
2 changes: 2 additions & 0 deletions dto/license_dto.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type LicenseEntitlements struct {
Webhooks bool `json:"webhooks"`
RuleSnoozes bool `json:"rule_snoozes"`
TestRun bool `json:"test_run"`
Sanctions bool `json:"sanctions"`
}

func AdaptLicenseEntitlements(licenseEntitlements models.LicenseEntitlements) LicenseEntitlements {
Expand All @@ -31,6 +32,7 @@ func AdaptLicenseEntitlements(licenseEntitlements models.LicenseEntitlements) Li
Webhooks: licenseEntitlements.Webhooks,
RuleSnoozes: licenseEntitlements.RuleSnoozes,
TestRun: licenseEntitlements.TestRun,
Sanctions: licenseEntitlements.Sanctions,
}
}

Expand Down
48 changes: 48 additions & 0 deletions dto/organization_feature_access_dto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package dto

import (
"github.com/checkmarble/marble-backend/models"
"github.com/checkmarble/marble-backend/utils"
)

type APIOrganizationFeatureAccess struct {
TestRun string `json:"test_run"`
Workflows string `json:"workflows"`
Webhooks string `json:"webhooks"`
RuleSnoozes string `json:"rule_snoozes"`
Roles string `json:"roles"`
Analytics string `json:"analytics"`
Sanctions string `json:"sanctions"`
}

func AdaptOrganizationFeatureAccessDto(f models.OrganizationFeatureAccess) APIOrganizationFeatureAccess {
return APIOrganizationFeatureAccess{
TestRun: f.TestRun.String(),
Workflows: f.Workflows.String(),
Webhooks: f.Webhooks.String(),
RuleSnoozes: f.RuleSnoozes.String(),
Roles: f.Roles.String(),
Analytics: f.Analytics.String(),
Sanctions: f.Sanctions.String(),
}
}

type UpdateOrganizationFeatureAccessBodyDto struct {
TestRun *string `json:"test_run"`
Sanctions *string `json:"sanctions"`
}

func AdaptUpdateOrganizationFeatureAccessInput(f UpdateOrganizationFeatureAccessBodyDto, orgId string) models.UpdateOrganizationFeatureAccessInput {
var testRun, sanctions *models.FeatureAccess
if f.TestRun != nil {
testRun = utils.Ptr(models.FeatureAccessFrom(*f.TestRun))
}
if f.Sanctions != nil {
sanctions = utils.Ptr(models.FeatureAccessFrom(*f.Sanctions))
}
return models.UpdateOrganizationFeatureAccessInput{
OrganizationId: orgId,
TestRun: testRun,
Sanctions: sanctions,
}
}
38 changes: 38 additions & 0 deletions models/feature_access.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package models

type FeatureAccess int

const (
Restricted FeatureAccess = iota
Allowed
Test
UnknownFeatureAccess
)

var ValidFeaturesAccess = []FeatureAccess{Allowed, Restricted, Test}

// Provide a string value for each outcome
func (f FeatureAccess) String() string {
switch f {
case Allowed:
return "allowed"
case Restricted:
return "restricted"
case Test:
return "test"
}
return "unknown"
}

// Provide an Outcome from a string value
func FeatureAccessFrom(s string) FeatureAccess {
switch s {
case "allowed":
return Allowed
case "restricted":
return Restricted
case "test":
return Test
}
return UnknownFeatureAccess
}
2 changes: 2 additions & 0 deletions models/license.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ type LicenseEntitlements struct {
Webhooks bool
RuleSnoozes bool
TestRun bool
Sanctions bool
}

type LicenseValidation struct {
Expand All @@ -78,6 +79,7 @@ func NewFullLicense() LicenseValidation {
Webhooks: true,
RuleSnoozes: true,
TestRun: true,
Sanctions: true,
},
}
}
Expand Down
69 changes: 69 additions & 0 deletions models/organization_feature_access.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package models

import "time"

type OrganizationFeatureAccess struct {
Id string
OrganizationId string
TestRun FeatureAccess
Workflows FeatureAccess
Webhooks FeatureAccess
RuleSnoozes FeatureAccess
Roles FeatureAccess
Analytics FeatureAccess
Sanctions FeatureAccess
CreatedAt time.Time
UpdatedAt time.Time
}

type DbStoredOrganizationFeatureAccess struct {
Id string
OrganizationId string
carere marked this conversation as resolved.
Show resolved Hide resolved
TestRun FeatureAccess
carere marked this conversation as resolved.
Show resolved Hide resolved
Sanctions FeatureAccess
CreatedAt time.Time
UpdatedAt time.Time
}

type UpdateOrganizationFeatureAccessInput struct {
OrganizationId string
TestRun *FeatureAccess
Sanctions *FeatureAccess
}

func (f DbStoredOrganizationFeatureAccess) MergeWithLicenseEntitlement(l *LicenseEntitlements) OrganizationFeatureAccess {
o := OrganizationFeatureAccess{
Id: f.Id,
OrganizationId: f.OrganizationId,
TestRun: f.TestRun,
Sanctions: f.Sanctions,
CreatedAt: f.CreatedAt,
UpdatedAt: f.UpdatedAt,
}
// First, set the feature accesses to "allowed" if the license allows it
if l.Analytics {
o.Analytics = Allowed
}
if l.Webhooks {
o.Webhooks = Allowed
}
if l.Workflows {
o.Workflows = Allowed
}
if l.RuleSnoozes {
o.RuleSnoozes = Allowed
}
if l.UserRoles {
o.Roles = Allowed
}

// remove the feature accesses that are not allowed by the license
if !l.TestRun {
o.TestRun = Restricted
}
if !l.Sanctions {
o.Sanctions = Restricted
}

return o
}
2 changes: 2 additions & 0 deletions repositories/dbmodels/db_license.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type DBLicense struct {
Webhooks bool `db:"webhooks"`
RuleSnoozes bool `db:"rule_snoozes"`
TestRun bool `db:"test_run"`
Sanctions bool `db:"sanctions"`
}

const TABLE_LICENSES = "licenses"
Expand All @@ -49,6 +50,7 @@ func AdaptLicense(db DBLicense) (models.License, error) {
Webhooks: db.Webhooks,
RuleSnoozes: db.RuleSnoozes,
TestRun: db.TestRun,
Sanctions: db.Sanctions,
},
}, nil
}
32 changes: 32 additions & 0 deletions repositories/dbmodels/db_organization_feature_access.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package dbmodels

import (
"time"

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

const TABLE_ORGANIZATION_FEATURE_ACCESS = "organization_feature_access"

var SelectOrganizationFeatureAccessColumn = utils.ColumnList[DBOrganizationFeatureAccess]()

type DBOrganizationFeatureAccess struct {
Id string `db:"id"`
OrganizationId string `db:"org_id"`
TestRun string `db:"test_run"`
Sanctions string `db:"sanctions"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}

func AdaptOrganizationFeatureAccess(db DBOrganizationFeatureAccess) (models.DbStoredOrganizationFeatureAccess, error) {
return models.DbStoredOrganizationFeatureAccess{
Id: db.Id,
OrganizationId: db.OrganizationId,
TestRun: models.FeatureAccessFrom(db.TestRun),
Sanctions: models.FeatureAccessFrom(db.Sanctions),
CreatedAt: db.CreatedAt,
UpdatedAt: db.UpdatedAt,
}, nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE
organization_feature_access (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4 (),
org_id UUID NOT NULL,
test_run VARCHAR NOT NULL DEFAULT 'allowed',
sanctions VARCHAR NOT NULL DEFAULT 'allowed',
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_org FOREIGN KEY (org_id) REFERENCES organizations (id) ON DELETE CASCADE
);

INSERT INTO
organization_feature_access (org_id)
SELECT
id
FROM
organizations;

CREATE UNIQUE INDEX unique_organization_feature_access ON organization_feature_access (org_id);

ALTER TABLE licenses
ADD COLUMN sanctions BOOLEAN NOT NULL DEFAULT FALSE;

-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP INDEX unique_organization_feature_access;

DROP TABLE organization_feature_access;

ALTER TABLE licenses
DROP COLUMN sanctions;

-- +goose StatementEnd
Loading
Loading