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

[CLOUDTRUST-2375] Add middleware to enable feature according to reaml conf. #41

Merged
merged 1 commit into from
Mar 4, 2020
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
32 changes: 32 additions & 0 deletions configuration/dto.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@ package configuration

import "encoding/json"

// Constants
const (
CheckKeyIDNow = "IDNow"
CheckKeyPhysical = "physical-check"
)

var (
// AvailableCheckKeys lists all available check keys for RealmAdminConfiguration
AvailableCheckKeys = []string{CheckKeyIDNow, CheckKeyPhysical}
)

// RealmConfiguration struct. APISelfAccountEditingEnabled replaces former field APISelfMailEditingEnabled
type RealmConfiguration struct {
DefaultClientID *string `json:"default_client_id,omitempty"`
Expand All @@ -21,6 +32,20 @@ type RealmConfiguration struct {
RedirectSuccessfulRegistrationURL *string `json:"redirect_successful_registration_url,omitempty"`
}

// RealmAdminConfiguration struct
type RealmAdminConfiguration struct {
Mode *string `json:"mode"`
AvailableChecks map[string]bool `json:"available-checks,omitempty"`
Accreditations []RealmAdminAccreditation `json:"accreditations,omitempty"`
}

// RealmAdminAccreditation struct
type RealmAdminAccreditation struct {
Type *string `json:"type,omitempty"`
Validity *string `json:"validity,omitempty"`
Condition *string `json:"condition,omitempty"`
}

// Authorization struct
type Authorization struct {
RealmID *string `json:"realm_id"`
Expand All @@ -43,3 +68,10 @@ func NewRealmConfiguration(confJSON string) (RealmConfiguration, error) {
conf.DeprecatedAPISelfMailEditingEnabled = nil
return conf, nil
}

// NewRealmAdminConfiguration returns the realm admin configuration from its JSON representation
func NewRealmAdminConfiguration(configJSON string) (RealmAdminConfiguration, error) {
var conf RealmAdminConfiguration
var err = json.Unmarshal([]byte(configJSON), &conf)
return conf, err
}
11 changes: 11 additions & 0 deletions configuration/dto_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,14 @@ func TestNewRealmConfiguration(t *testing.T) {
assert.False(t, *conf.APISelfAccountEditingEnabled)
})
}

func TestNewRealmAdminConfiguration(t *testing.T) {
t.Run("Invalid JSON", func(t *testing.T) {
var _, err = NewRealmAdminConfiguration(`{`)
assert.NotNil(t, err)
})
t.Run("Valid JSON", func(t *testing.T) {
var _, err = NewRealmAdminConfiguration(`{}`)
assert.Nil(t, err)
})
}
3 changes: 3 additions & 0 deletions configuration/mock_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package configuration

//go:generate mockgen -destination=./mock/cloudtrustdb.go -package=mock -mock_names=CloudtrustDB=CloudtrustDB,SQLRow=SQLRow github.com/cloudtrust/common-service/database/sqltypes CloudtrustDB,SQLRow
20 changes: 18 additions & 2 deletions configuration/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import (
)

const (
selectConfigStmt = `SELECT configuration FROM realm_configuration WHERE (realm_id = ?)`
selectAllAuthzStmt = `SELECT realm_id, group_name, action, target_realm_id, target_group_name FROM authorizations;`
selectConfigStmt = `SELECT configuration FROM realm_configuration WHERE realm_id = ? AND configuration IS NOT NULL`
selectAdminConfigStmt = `SELECT admin_configuration FROM realm_configuration WHERE realm_id = ? AND admin_configuration IS NOT NULL`
selectAllAuthzStmt = `SELECT realm_id, group_name, action, target_realm_id, target_group_name FROM authorizations;`
)

// ConfigurationReaderDBModule struct
Expand Down Expand Up @@ -46,6 +47,21 @@ func (c *ConfigurationReaderDBModule) GetConfiguration(ctx context.Context, real
}
}

// GetAdminConfiguration returns a realm admin configuration
func (c *ConfigurationReaderDBModule) GetAdminConfiguration(ctx context.Context, realmID string) (RealmAdminConfiguration, error) {
var configJSON string
row := c.db.QueryRow(selectAdminConfigStmt, realmID)

var err = row.Scan(&configJSON)
if err != nil {
if err == sql.ErrNoRows {
c.logger.Warn(ctx, "msg", "Realm Admin Configuration not found in DB", "error", err.Error())
}
return RealmAdminConfiguration{}, err
}
return NewRealmAdminConfiguration(configJSON)
}

// GetAuthorizations returns authorizations
func (c *ConfigurationReaderDBModule) GetAuthorizations(ctx context.Context) ([]Authorization, error) {
// Get Authorizations from DB
Expand Down
83 changes: 83 additions & 0 deletions configuration/module_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package configuration

import (
"context"
"database/sql"
"errors"
"testing"

"github.com/cloudtrust/common-service/configuration/mock"
"github.com/cloudtrust/common-service/log"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
)

func TestGetConfiguration(t *testing.T) {
var mockCtrl = gomock.NewController(t)
defer mockCtrl.Finish()

var mockDB = mock.NewCloudtrustDB(mockCtrl)
var mockSQLRow = mock.NewSQLRow(mockCtrl)
var logger = log.NewNopLogger()

var realmID = "myrealm"
var ctx = context.TODO()
var module = NewConfigurationReaderDBModule(mockDB, logger)

t.Run("SQL error", func(t *testing.T) {
mockDB.EXPECT().QueryRow(gomock.Any(), realmID).Return(mockSQLRow)
mockSQLRow.EXPECT().Scan(gomock.Any()).Return(errors.New("SQL error"))
var _, err = module.GetConfiguration(ctx, realmID)
assert.NotNil(t, err)
})
t.Run("SQL No row", func(t *testing.T) {
mockDB.EXPECT().QueryRow(gomock.Any(), realmID).Return(mockSQLRow)
mockSQLRow.EXPECT().Scan(gomock.Any()).Return(sql.ErrNoRows)
var _, err = module.GetConfiguration(ctx, realmID)
assert.NotNil(t, err)
})
t.Run("Success", func(t *testing.T) {
mockDB.EXPECT().QueryRow(gomock.Any(), realmID).Return(mockSQLRow)
mockSQLRow.EXPECT().Scan(gomock.Any()).DoAndReturn(func(ptrConfig *string) error {
*ptrConfig = `{}`
return nil
})
var _, err = module.GetConfiguration(ctx, realmID)
assert.Nil(t, err)
})
}

func TestGetAdminConfiguration(t *testing.T) {
var mockCtrl = gomock.NewController(t)
defer mockCtrl.Finish()

var mockDB = mock.NewCloudtrustDB(mockCtrl)
var mockSQLRow = mock.NewSQLRow(mockCtrl)
var logger = log.NewNopLogger()

var realmID = "myrealm"
var ctx = context.TODO()
var module = NewConfigurationReaderDBModule(mockDB, logger)

t.Run("SQL error", func(t *testing.T) {
mockDB.EXPECT().QueryRow(gomock.Any(), realmID).Return(mockSQLRow)
mockSQLRow.EXPECT().Scan(gomock.Any()).Return(errors.New("SQL error"))
var _, err = module.GetAdminConfiguration(ctx, realmID)
assert.NotNil(t, err)
})
t.Run("SQL No row", func(t *testing.T) {
mockDB.EXPECT().QueryRow(gomock.Any(), realmID).Return(mockSQLRow)
mockSQLRow.EXPECT().Scan(gomock.Any()).Return(sql.ErrNoRows)
var _, err = module.GetAdminConfiguration(ctx, realmID)
assert.NotNil(t, err)
})
t.Run("Success", func(t *testing.T) {
mockDB.EXPECT().QueryRow(gomock.Any(), realmID).Return(mockSQLRow)
mockSQLRow.EXPECT().Scan(gomock.Any()).DoAndReturn(func(ptrConfig *string) error {
*ptrConfig = `{}`
return nil
})
var _, err = module.GetAdminConfiguration(ctx, realmID)
assert.Nil(t, err)
})
}
9 changes: 9 additions & 0 deletions errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const (
MsgErrInvalidPathParam = "invalidPathParameter"
MsgErrInvalidParam = "invalidParameter"
MsgErrOpNotPermitted = "operationNotPermitted"
MsgErrDisabledEndpoint = "disabledEndpoint"

AuthHeader = "authorizationHeader"
BasicToken = "basicToken"
Expand Down Expand Up @@ -98,3 +99,11 @@ func CreateNotFoundError(messageKey string) Error {
Message: GetEmitter() + "." + messageKey,
}
}

// CreateEndpointNotEnabled creates an error relative to an attempt to access a not enabled endpoint
func CreateEndpointNotEnabled(param string) Error {
return Error{
Status: http.StatusConflict,
Message: fmt.Sprintf("%s.%s.%s", GetEmitter(), MsgErrDisabledEndpoint, param),
}
}
2 changes: 0 additions & 2 deletions middleware/authentication_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package middleware

//go:generate mockgen -destination=./mock/keycloak_client.go -package=mock -mock_names=KeycloakClient=KeycloakClient github.com/cloudtrust/common-service/middleware KeycloakClient

import (
"bytes"
"context"
Expand Down
55 changes: 55 additions & 0 deletions middleware/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package middleware

import (
"context"
"errors"
"net/http"
"time"

cs "github.com/cloudtrust/common-service"
"github.com/cloudtrust/common-service/configuration"
errorhandler "github.com/cloudtrust/common-service/errors"
"github.com/cloudtrust/common-service/log"
"github.com/cloudtrust/common-service/metrics"
Expand Down Expand Up @@ -37,6 +39,59 @@ func MakeEndpointInstrumentingMW(m metrics.Metrics, histoName string) cs.Middlew
}
}

// IDRetriever is an interface to get an ID using an object's name
type IDRetriever interface {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we need this abstraction ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why do we need this abstraction ?

Because implementation is done in keycloak-client and keycloak-client refers to common-service... So we should not add a reference to keycloak-client in this library

GetID(accessToken, name string) (string, error)
}

// AdminConfigurationRetriever is an interface to get an admin configuration
type AdminConfigurationRetriever interface {
GetAdminConfiguration(ctx context.Context, realmID string) (configuration.RealmAdminConfiguration, error)
}

// MakeEndpointAvailableCheckMW makes a middleware that ensure a feature is enabled at admin configuration level in the current context
func MakeEndpointAvailableCheckMW(enabledKey string, idRetriever IDRetriever, confRetriever AdminConfigurationRetriever, logger log.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
var ctx = req.Context()
var realmName = ctx.Value(cs.CtContextRealm).(string)
var accessToken = ctx.Value(cs.CtContextAccessToken).(string)
// Get realm ID
var realmID, err = idRetriever.GetID(accessToken, realmName)
if err != nil {
logger.Info(ctx, "msg", "Can't get realm ID", "realm", realmName)
handleError(req.Context(), err, w)
return
}
// Get admin configuration
var conf configuration.RealmAdminConfiguration
conf, err = confRetriever.GetAdminConfiguration(ctx, realmID)
if err != nil {
logger.Info(ctx, "msg", "Can't get realm admin configuration", "realm", realmName)
handleError(req.Context(), err, w)
return
}
if !conf.AvailableChecks[enabledKey] {
logger.Info(ctx, "msg", "Feature not enabled", "realm", realmName, "feat", enabledKey)
handleError(req.Context(), errorhandler.CreateEndpointNotEnabled(realmName), w)
return
}

ctx = context.WithValue(ctx, cs.CtContextRealmID, realmID)
next.ServeHTTP(w, req.WithContext(ctx))
})
}
}

func handleError(ctx context.Context, err error, w http.ResponseWriter) {
switch e := err.(type) {
case errorhandler.Error:
httpErrorHandler(ctx, e.Status, e, w)
default:
httpErrorHandler(ctx, http.StatusInternalServerError, errors.New("unexpected.error"), w)
}
}

func httpErrorHandler(_ context.Context, statusCode int, err error, w http.ResponseWriter) {
w.WriteHeader(statusCode)
w.Header().Set("Content-Type", "application/json; charset=utf-8")
Expand Down
Loading