Skip to content
Draft
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
4 changes: 2 additions & 2 deletions internal/controller/httpapi/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,13 @@ func NewRouter(handler *gin.Engine, l logger.Interface, t usecase.Usecases, cfg
{
v1.NewDeviceRoutes(h2, t.Devices, l)
v1.NewAmtRoutes(h2, t.Devices, t.AMTExplorer, t.Exporter, l)
v1.NewCIRACertRoutes(h2, l)
v1.NewCIRACertRoutes(h2, l, cfg)
}

h := protected.Group("/v1/admin")
{
v1.NewDomainRoutes(h, t.Domains, l)
v1.NewCIRAConfigRoutes(h, t.CIRAConfigs, l)
v1.NewCIRAConfigRoutes(h, t.CIRAConfigs, l, cfg)
v1.NewProfileRoutes(h, t.Profiles, l)
v1.NewWirelessConfigRoutes(h, t.WirelessProfiles, l)
v1.NewIEEE8021xConfigRoutes(h, t.IEEE8021xProfiles, l)
Expand Down
16 changes: 5 additions & 11 deletions internal/controller/httpapi/v1/ciracert.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/gin-gonic/gin"

"github.com/device-management-toolkit/console/config"
"github.com/device-management-toolkit/console/pkg/logger"
)

Expand All @@ -31,26 +32,19 @@ type ciraCertRoutes struct {
certReader CertReader
}

func NewCIRACertRoutes(handler *gin.RouterGroup, l logger.Interface) {
r := &ciraCertRoutes{
l: l,
certReader: &FileCertReader{Path: "config/root_cert.pem"},
}

h := handler.Group("/ciracert")
{
h.GET("", r.getCIRACert)
}
func NewCIRACertRoutes(handler *gin.RouterGroup, l logger.Interface, cfg *config.Config) {
NewCIRACertRoutesWithReader(handler, l, &FileCertReader{Path: "config/root_cert.pem"}, cfg)
}

// NewCIRACertRoutesWithReader creates routes with a custom cert reader (for testing).
func NewCIRACertRoutesWithReader(handler *gin.RouterGroup, l logger.Interface, certReader CertReader) {
func NewCIRACertRoutesWithReader(handler *gin.RouterGroup, l logger.Interface, certReader CertReader, cfg *config.Config) {
r := &ciraCertRoutes{
l: l,
certReader: certReader,
}

h := handler.Group("/ciracert")
h.Use(ciraDisabledMiddleware(cfg.DisableCIRA))
{
h.GET("", r.getCIRACert)
}
Expand Down
25 changes: 24 additions & 1 deletion internal/controller/httpapi/v1/ciracert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"

"github.com/device-management-toolkit/console/config"
"github.com/device-management-toolkit/console/pkg/logger"
)

Expand All @@ -32,7 +33,7 @@ func ciraCertTestWithReader(t *testing.T, reader CertReader) *gin.Engine {
engine := gin.New()
handler := engine.Group("/api/v1/admin")

NewCIRACertRoutesWithReader(handler, log, reader)
NewCIRACertRoutesWithReader(handler, log, reader, &config.Config{})

return engine
}
Expand Down Expand Up @@ -233,6 +234,28 @@ func TestCIRACertRoutes_ReadError(t *testing.T) {
require.Contains(t, w.Body.String(), "Failed to read certificate file")
}

func TestCIRACertRoutes_DisabledReturns403(t *testing.T) {
t.Parallel()

log := logger.New("error")

engine := gin.New()
handler := engine.Group("/api/v1/admin")

// Reader errors if reached — the guard must reject first.
reader := &mockCertReader{data: nil, err: errors.New("should not be read")}
NewCIRACertRoutesWithReader(handler, log, reader, &config.Config{App: config.App{DisableCIRA: true}})

req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/api/v1/admin/ciracert", http.NoBody)
require.NoError(t, err)

w := httptest.NewRecorder()
engine.ServeHTTP(w, req)

require.Equal(t, http.StatusForbidden, w.Code)
require.JSONEq(t, `{"error":"CIRA is disabled on this instance"}`, w.Body.String())
}

// TestCIRACertRoutes_Coverage ensures all code paths are tested.
func TestCIRACertRoutes_Coverage(t *testing.T) {
t.Parallel()
Expand Down
16 changes: 9 additions & 7 deletions internal/controller/httpapi/v1/ciraconfigs.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/gin-gonic/gin"

"github.com/device-management-toolkit/console/config"
"github.com/device-management-toolkit/console/internal/entity/dto/v1"
"github.com/device-management-toolkit/console/internal/usecase/ciraconfigs"
"github.com/device-management-toolkit/console/pkg/logger"
Expand All @@ -15,10 +16,11 @@ type ciraConfigRoutes struct {
l logger.Interface
}

func NewCIRAConfigRoutes(handler *gin.RouterGroup, t ciraconfigs.Feature, l logger.Interface) {
func NewCIRAConfigRoutes(handler *gin.RouterGroup, t ciraconfigs.Feature, l logger.Interface, cfg *config.Config) {
r := &ciraConfigRoutes{t, l}

h := handler.Group("/ciraconfigs")
h.Use(ciraDisabledMiddleware(cfg.DisableCIRA))
{
h.GET("", r.get)
h.GET(":ciraConfigName", r.getByName)
Expand Down Expand Up @@ -80,15 +82,15 @@ func (r *ciraConfigRoutes) getByName(c *gin.Context) {
}

func (r *ciraConfigRoutes) insert(c *gin.Context) {
var config dto.CIRAConfig
if err := c.ShouldBindJSON(&config); err != nil {
var ciraConfig dto.CIRAConfig
if err := c.ShouldBindJSON(&ciraConfig); err != nil {
r.l.Error(err, "http - CIRA configs - v1 - insert")
ErrorResponse(c, err)

return
}

newCiraConfig, err := r.cira.Insert(c.Request.Context(), &config)
newCiraConfig, err := r.cira.Insert(c.Request.Context(), &ciraConfig)
if err != nil {
r.l.Error(err, "http - CIRA configs - v1 - insert")
ErrorResponse(c, err)
Expand All @@ -100,15 +102,15 @@ func (r *ciraConfigRoutes) insert(c *gin.Context) {
}

func (r *ciraConfigRoutes) update(c *gin.Context) {
var config dto.CIRAConfig
if err := c.ShouldBindJSON(&config); err != nil {
var ciraConfig dto.CIRAConfig
if err := c.ShouldBindJSON(&ciraConfig); err != nil {
r.l.Error(err, "http - CIRA configs - v1 - update")
ErrorResponse(c, err)

return
}

updatedConfig, err := r.cira.Update(c.Request.Context(), &config)
updatedConfig, err := r.cira.Update(c.Request.Context(), &ciraConfig)
if err != nil {
r.l.Error(err, "http - CIRA configs - v1 - update")
ErrorResponse(c, err)
Expand Down
46 changes: 45 additions & 1 deletion internal/controller/httpapi/v1/ciraconfigs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"

"github.com/device-management-toolkit/console/config"
"github.com/device-management-toolkit/console/internal/entity/dto/v1"
"github.com/device-management-toolkit/console/internal/mocks"
"github.com/device-management-toolkit/console/internal/usecase/ciraconfigs"
Expand All @@ -30,7 +31,7 @@ func ciraconfigsTest(t *testing.T) (*mocks.MockCIRAConfigsFeature, *gin.Engine)
engine := gin.New()
handler := engine.Group("/api/v1/admin")

NewCIRAConfigRoutes(handler, ciraconfig, log)
NewCIRAConfigRoutes(handler, ciraconfig, log, &config.Config{})

return ciraconfig, engine
}
Expand Down Expand Up @@ -296,3 +297,46 @@ func TestCIRAConfigRoutes(t *testing.T) {
})
}
}

func TestCIRAConfigRoutes_DisabledReturns403(t *testing.T) {
t.Parallel()

log := logger.New("error")

engine := gin.New()
handler := engine.Group("/api/v1/admin")

// No expectations: the guard must reject before the feature is reached.
mockCtl := gomock.NewController(t)
ciraconfig := mocks.NewMockCIRAConfigsFeature(mockCtl)

NewCIRAConfigRoutes(handler, ciraconfig, log, &config.Config{App: config.App{DisableCIRA: true}})

cases := []struct {
method string
url string
}{
{http.MethodGet, "/api/v1/admin/ciraconfigs"},
{http.MethodGet, "/api/v1/admin/ciraconfigs/example"},
{http.MethodPost, "/api/v1/admin/ciraconfigs"},
{http.MethodPatch, "/api/v1/admin/ciraconfigs"},
{http.MethodDelete, "/api/v1/admin/ciraconfigs/example"},
}

for _, tc := range cases {
tc := tc

t.Run(tc.method+" "+tc.url, func(t *testing.T) {
t.Parallel()

req, err := http.NewRequestWithContext(context.Background(), tc.method, tc.url, http.NoBody)
require.NoError(t, err)
Comment on lines +309 to +333

w := httptest.NewRecorder()
engine.ServeHTTP(w, req)

require.Equal(t, http.StatusForbidden, w.Code)
require.JSONEq(t, `{"error":"CIRA is disabled on this instance"}`, w.Body.String())
})
}
}
23 changes: 23 additions & 0 deletions internal/controller/httpapi/v1/ciradisabled.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package v1

import (
"net/http"

"github.com/gin-gonic/gin"
)

// ciraDisabledMessage is returned when CIRA is disabled (APP_DISABLE_CIRA=true).
const ciraDisabledMessage = "CIRA is disabled on this instance"

// ciraDisabledMiddleware returns 403 for CIRA endpoints when CIRA is disabled.
func ciraDisabledMiddleware(disabled bool) gin.HandlerFunc {
return func(c *gin.Context) {
if disabled {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{errorKey: ciraDisabledMessage})

return
}

c.Next()
}
}
10 changes: 5 additions & 5 deletions internal/controller/openapi/cira.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,30 +16,30 @@ func (f *FuegoAdapter) RegisterCIRAConfigRoutes() {
fuego.OptionQueryInt("$top", "Number of records to return"),
fuego.OptionQueryInt("$skip", "Number of records to skip"),
fuego.OptionQueryBool("$count", "Include total count"),
protectedRouteOptions(),
ciraProtectedRouteOptions(),
)

fuego.Get(f.server, "/api/v1/admin/ciraconfigs/{ciraConfigName}", f.getCIRAConfigByName,
fuego.OptionTags("CIRA"),
fuego.OptionSummary("Get CIRA Configuration by Name"),
fuego.OptionDescription("Retrieve a specific CIRA configuration by profile name"),
fuego.OptionPath("ciraConfigName", "Profile name"),
protectedRouteOptions(),
ciraProtectedRouteOptions(),
)

fuego.Post(f.server, "/api/v1/admin/ciraconfigs", f.createCIRAConfig,
fuego.OptionTags("CIRA"),
fuego.OptionSummary("Create CIRA Configuration"),
fuego.OptionDescription("Create a new CIRA configuration"),
fuego.OptionDefaultStatusCode(http.StatusCreated),
protectedRouteOptions(),
ciraProtectedRouteOptions(),
)

fuego.Patch(f.server, "/api/v1/admin/ciraconfigs", f.updateCIRAConfig,
fuego.OptionTags("CIRA"),
fuego.OptionSummary("Update CIRA Configuration"),
fuego.OptionDescription("Update an existing CIRA configuration"),
protectedRouteOptions(),
ciraProtectedRouteOptions(),
)

fuego.Delete(f.server, "/api/v1/admin/ciraconfigs/{ciraConfigName}", f.deleteCIRAConfig,
Expand All @@ -48,7 +48,7 @@ func (f *FuegoAdapter) RegisterCIRAConfigRoutes() {
fuego.OptionDescription("Delete a CIRA configuration by profile name"),
fuego.OptionPath("ciraConfigName", "Profile name"),
fuego.OptionDefaultStatusCode(http.StatusNoContent),
protectedRouteOptions(),
ciraProtectedRouteOptions(),
)
}

Expand Down
2 changes: 1 addition & 1 deletion internal/controller/openapi/ciracert.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ func (f *FuegoAdapter) RegisterCIRACertRoutes() {
fuego.OptionSummary("Get CIRA Root Certificate"),
fuego.OptionDescription("Retrieve the root CIRA certificate as plain text"),
fuego.OptionAddResponse(http.StatusOK, "OK", fuego.Response{Type: "", ContentTypes: []string{"text/plain"}}),
protectedRouteOptions(),
ciraProtectedRouteOptions(),
)
}

Expand Down
8 changes: 8 additions & 0 deletions internal/controller/openapi/route_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ func protectedRouteOptions() fuego.RouteOption {
)
}

// ciraProtectedRouteOptions is protectedRouteOptions plus the 403 returned when CIRA is disabled.
func ciraProtectedRouteOptions() fuego.RouteOption {
return routeOptionGroup(
protectedRouteOptions(),
errorResponseOption(http.StatusForbidden, "Forbidden _(CIRA is disabled on this instance)_"),
)
}

func errorResponseOption(statusCode int, description string) fuego.RouteOption {
return fuego.OptionAddResponse(statusCode, description, fuego.Response{Type: fuego.HTTPError{}})
}
Expand Down
Loading