feat: load options from db

This commit is contained in:
Denis Gukov 2024-07-07 22:12:21 +05:00
parent b0e766355a
commit 7195913a5f
21 changed files with 324 additions and 63 deletions

View File

@ -58,7 +58,7 @@ func ReceiveIntegration(w http.ResponseWriter, r *http.Request) {
var integrations []db.Integration
if util.Config.GlobalIntegrationAlias != "" && integrationAlias == util.Config.GlobalIntegrationAlias {
if util.Config.IntegrationAlias != "" && integrationAlias == util.Config.IntegrationAlias {
integrations, err = helpers.Store(r).GetAllSearchableIntegrations()
} else {
integrations, err = helpers.Store(r).GetIntegrationsByAlias(integrationAlias)

View File

@ -65,7 +65,7 @@ func tryFindLDAPUser(username, password string) (*db.User, error) {
return nil, err
}
// Search for the given username
// Filter for the given username
searchRequest := ldap.NewSearchRequest(
util.Config.LdapSearchDN,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,

View File

@ -7,6 +7,32 @@ import (
"net/http"
)
func setOption(w http.ResponseWriter, r *http.Request) {
currentUser := context.Get(r, "user").(*db.User)
if !currentUser.Admin {
helpers.WriteJSON(w, http.StatusForbidden, map[string]string{
"error": "User must be admin",
})
return
}
var option db.Option
if !helpers.Bind(w, r, &option) {
return
}
err := helpers.Store(r).SetOption(option.Key, option.Value)
if err != nil {
helpers.WriteJSON(w, http.StatusInternalServerError, map[string]string{
"error": "Can not set option",
})
return
}
helpers.WriteJSON(w, http.StatusOK, option)
}
func getOptions(w http.ResponseWriter, r *http.Request) {
currentUser := context.Get(r, "user").(*db.User)
@ -16,4 +42,14 @@ func getOptions(w http.ResponseWriter, r *http.Request) {
})
return
}
options, err := helpers.Store(r).GetOptions(db.RetrieveQueryParams{})
if err != nil {
helpers.WriteJSON(w, http.StatusInternalServerError, map[string]string{
"error": "Can not get options",
})
return
}
helpers.WriteJSON(w, http.StatusOK, options)
}

View File

@ -122,6 +122,9 @@ func Route() *mux.Router {
tokenAPI.Path("/tokens").HandlerFunc(createAPIToken).Methods("POST")
tokenAPI.HandleFunc("/tokens/{token_id}", expireAPIToken).Methods("DELETE")
authenticatedAPI.Path("/options").HandlerFunc(getOptions).Methods("GET", "HEAD")
authenticatedAPI.Path("/options").HandlerFunc(setOption).Methods("POST", "HEAD")
userAPI := authenticatedAPI.Path("/users/{user_id}").Subrouter()
userAPI.Use(getUserMiddleware)

View File

@ -22,11 +22,33 @@ func getUser(w http.ResponseWriter, r *http.Request) {
var user struct {
db.User
CanCreateProject bool `json:"can_create_project"`
//Apps []db.AppPublic `json:"apps"`
}
user.User = *context.Get(r, "user").(*db.User)
user.CanCreateProject = user.Admin || util.Config.NonAdminCanCreateProject
//str, err := helpers.Store(r).GetOption("apps")
//if err != nil {
// w.WriteHeader(http.StatusInternalServerError)
// return
//}
//var apps []db.App
//err = json.Unmarshal([]byte(str), &apps)
//if err != nil {
// w.WriteHeader(http.StatusInternalServerError)
// return
//}
//
//for _, app := range apps {
// if app.Mode == db.AppActive {
// user.Apps = append(user.Apps, app.AppPublic)
//
// }
//}
helpers.WriteJSON(w, http.StatusOK, user)
}

View File

@ -108,5 +108,11 @@ func createStore(token string) db.Store {
panic(err)
}
err = db.FillConfigFromDB(store)
if err != nil {
panic(err)
}
return store
}

View File

@ -38,6 +38,7 @@ type RetrieveQueryParams struct {
Count int
SortBy string
SortInverted bool
Filter string
}
type ObjectReferrer struct {
@ -109,7 +110,7 @@ type Store interface {
// if a rollback exists
TryRollbackMigration(version Migration)
GetOptions() (map[string]string, error)
GetOptions(params RetrieveQueryParams) (map[string]string, error)
GetOption(key string) (string, error)
SetOption(key string, value string) error
@ -487,3 +488,25 @@ func ValidateInventory(store Store, inventory *Inventory) (err error) {
return
}
func ConvertFlatToNested(flatMap map[string]string) map[string]interface{} {
nestedMap := make(map[string]interface{})
for key, value := range flatMap {
parts := strings.Split(key, ".")
currentMap := nestedMap
for i, part := range parts {
if i == len(parts)-1 {
currentMap[part] = value
} else {
if _, exists := currentMap[part]; !exists {
currentMap[part] = make(map[string]interface{})
}
currentMap = currentMap[part].(map[string]interface{})
}
}
}
return nestedMap
}

View File

@ -5,7 +5,7 @@ import (
"github.com/ansible-semaphore/semaphore/db"
)
func (d *BoltDb) GetOptions() (res map[string]string, err error) {
func (d *BoltDb) GetOptions(params db.RetrieveQueryParams) (res map[string]string, err error) {
var options []db.Option
err = d.getObjects(0, db.OptionProps, db.RetrieveQueryParams{}, nil, &options)
for _, opt := range options {

99
db/config.go Normal file
View File

@ -0,0 +1,99 @@
package db
import (
"fmt"
"github.com/ansible-semaphore/semaphore/util"
"reflect"
"strings"
)
func assignMapToStruct[P *S, S any](m map[string]interface{}, s P) error {
v := reflect.ValueOf(s).Elem()
return assignMapToStructRecursive(m, v)
}
func assignMapToStructRecursive(m map[string]interface{}, structValue reflect.Value) error {
structType := structValue.Type()
for i := 0; i < structType.NumField(); i++ {
field := structType.Field(i)
jsonTag := field.Tag.Get("json")
if jsonTag == "" {
jsonTag = field.Name
} else {
jsonTag = strings.Split(jsonTag, ",")[0]
}
value, ok := m[jsonTag]
if !ok {
continue
}
fieldValue := structValue.FieldByName(field.Name)
if !fieldValue.CanSet() {
continue
}
val := reflect.ValueOf(value)
switch fieldValue.Kind() {
case reflect.Struct:
// Handle nested struct
if val.Kind() != reflect.Map {
return fmt.Errorf("expected map for nested struct field %s but got %T", field.Name, value)
}
mapValue, ok := value.(map[string]interface{})
if !ok {
return fmt.Errorf("cannot assign value of type %T to field %s of type %s", value, field.Name, field.Type)
}
err := assignMapToStructRecursive(mapValue, fieldValue)
if err != nil {
return err
}
case reflect.Map:
if val.Kind() != reflect.Map {
return fmt.Errorf("expected map for field %s but got %T", field.Name, value)
}
mapValue := reflect.MakeMap(fieldValue.Type())
for _, key := range val.MapKeys() {
mapElemValue := val.MapIndex(key)
mapValue.SetMapIndex(key, mapElemValue)
}
fieldValue.Set(mapValue)
default:
// Handle simple types
if val.Type().ConvertibleTo(fieldValue.Type()) {
fieldValue.Set(val.Convert(fieldValue.Type()))
} else {
return fmt.Errorf("cannot assign value of type %s to field %s of type %s",
val.Type(), field.Name, fieldValue.Type())
}
}
}
return nil
}
func FillConfigFromDB(store Store) (err error) {
opts, err := store.GetOptions(RetrieveQueryParams{})
if err != nil {
return
}
options := ConvertFlatToNested(opts)
if options["apps"] == nil {
options["apps"] = make(map[string]interface{})
}
err = assignMapToStruct(options, util.Config)
return
}

40
db/config_test.go Normal file
View File

@ -0,0 +1,40 @@
package db
import "testing"
func TestConfig_assignMapToStruct(t *testing.T) {
type Address struct {
Street string `json:"street"`
City string `json:"city"`
}
type User struct {
Name string `json:"name"`
Age int `json:"age"`
Email string `json:"email"`
Address Address `json:"address"`
Details map[string]string `json:"details"`
}
johnData := map[string]interface{}{
"name": "John Doe",
"age": 30,
"email": "john.doe@example.com",
"address": map[string]interface{}{
"street": "123 Main St",
"city": "Anytown",
},
"details": map[string]string{
"occupation": "engineer",
"hobby": "hiking",
},
}
var john User
err := assignMapToStruct(johnData, &john)
if err != nil {
t.Fatal(err)
}
}

View File

@ -235,8 +235,14 @@ func (d *SqlDb) makeObjectsQuery(projectID int, props db.ObjectProps, params db.
return q
}
func (d *SqlDb) getObjects(projectID int, props db.ObjectProps, params db.RetrieveQueryParams, objects interface{}) (err error) {
query, args, err := d.makeObjectsQuery(projectID, props, params).ToSql()
func (d *SqlDb) getObjects(projectID int, props db.ObjectProps, params db.RetrieveQueryParams, prepare func(squirrel.SelectBuilder) squirrel.SelectBuilder, objects interface{}) (err error) {
q := d.makeObjectsQuery(projectID, props, params)
if prepare != nil {
q = prepare(q)
}
query, args, err := q.ToSql()
if err != nil {
return

View File

@ -105,7 +105,7 @@ func (d *SqlDb) RekeyAccessKeys(oldKey string) (err error) {
for i := 0; ; i++ {
var keys []db.AccessKey
err = d.getObjects(-1, globalProps, db.RetrieveQueryParams{Count: RekeyBatchSize, Offset: i * RekeyBatchSize}, &keys)
err = d.getObjects(-1, globalProps, db.RetrieveQueryParams{Count: RekeyBatchSize, Offset: i * RekeyBatchSize}, nil, &keys)
if err != nil {
return

View File

@ -15,7 +15,7 @@ func (d *SqlDb) GetEnvironmentRefs(projectID int, environmentID int) (db.ObjectR
func (d *SqlDb) GetEnvironments(projectID int, params db.RetrieveQueryParams) ([]db.Environment, error) {
var environment []db.Environment
err := d.getObjects(projectID, db.EnvironmentProps, params, &environment)
err := d.getObjects(projectID, db.EnvironmentProps, params, nil, &environment)
return environment, err
}

View File

@ -37,7 +37,7 @@ func (d *SqlDb) CreateIntegration(integration db.Integration) (newIntegration db
}
func (d *SqlDb) GetIntegrations(projectID int, params db.RetrieveQueryParams) (integrations []db.Integration, err error) {
err = d.getObjects(projectID, db.IntegrationProps, params, &integrations)
err = d.getObjects(projectID, db.IntegrationProps, params, nil, &integrations)
return integrations, err
}

View File

@ -14,7 +14,7 @@ func (d *SqlDb) GetInventory(projectID int, inventoryID int) (inventory db.Inven
func (d *SqlDb) GetInventories(projectID int, params db.RetrieveQueryParams) ([]db.Inventory, error) {
var inventories []db.Inventory
err := d.getObjects(projectID, db.InventoryProps, params, &inventories)
err := d.getObjects(projectID, db.InventoryProps, params, nil, &inventories)
return inventories, err
}

View File

@ -3,8 +3,10 @@ package sql
import (
"database/sql"
"errors"
"fmt"
"github.com/Masterminds/squirrel"
"github.com/ansible-semaphore/semaphore/db"
"regexp"
)
func (d *SqlDb) SetOption(key string, value string) error {
@ -22,12 +24,37 @@ func (d *SqlDb) SetOption(key string, value string) error {
return err
}
func (d *SqlDb) GetOptions() (res map[string]string, err error) {
func (d *SqlDb) GetOptions(params db.RetrieveQueryParams) (res map[string]string, err error) {
var options []db.Option
err = d.getObjects(0, db.OptionProps, db.RetrieveQueryParams{}, &options)
if params.Filter != "" {
var m bool
m, err = regexp.Match(`^(?:\w.*)+$`, []byte(params.Filter))
if err != nil {
return
}
if !m {
err = fmt.Errorf("invalid filter format")
return
}
}
err = d.getObjects(0, db.OptionProps, params, func(q squirrel.SelectBuilder) squirrel.SelectBuilder {
if params.Filter == "" {
return q
}
return q.Where("`key` like ?", params.Filter+".%")
}, &options)
if err != nil {
return
}
for _, opt := range options {
res[opt.Key] = opt.Value
}
return
}

View File

@ -24,7 +24,7 @@ func (d *SqlDb) GetGlobalRunner(runnerID int) (runner db.Runner, err error) {
}
func (d *SqlDb) GetGlobalRunners() (runners []db.Runner, err error) {
err = d.getObjects(0, db.GlobalRunnerProps, db.RetrieveQueryParams{}, &runners)
err = d.getObjects(0, db.GlobalRunnerProps, db.RetrieveQueryParams{}, nil, &runners)
return
}

View File

@ -8,7 +8,7 @@ func (d *SqlDb) GetView(projectID int, viewID int) (view db.View, err error) {
}
func (d *SqlDb) GetViews(projectID int) (views []db.View, err error) {
err = d.getObjects(projectID, db.ViewProps, db.RetrieveQueryParams{}, &views)
err = d.getObjects(projectID, db.ViewProps, db.RetrieveQueryParams{}, nil, &views)
return
}

9
util/App.go Normal file
View File

@ -0,0 +1,9 @@
package util
type AppConfig struct {
Title string `json:"title"`
Icon string `json:"icon"`
Active bool `json:"active"`
AppPath string `json:"path"`
AppArgs map[string]string `json:"args"`
}

37
util/OdbcProvider.go Normal file
View File

@ -0,0 +1,37 @@
package util
type OidcProvider struct {
ClientID string `json:"client_id"`
ClientIDFile string `json:"client_id_file"`
ClientSecret string `json:"client_secret"`
ClientSecretFile string `json:"client_secret_file"`
RedirectURL string `json:"redirect_url"`
Scopes []string `json:"scopes"`
DisplayName string `json:"display_name"`
Color string `json:"color"`
Icon string `json:"icon"`
AutoDiscovery string `json:"provider_url"`
Endpoint oidcEndpoint `json:"endpoint"`
UsernameClaim string `json:"username_claim" default:"preferred_username"`
NameClaim string `json:"name_claim" default:"preferred_username"`
EmailClaim string `json:"email_claim" default:"email"`
Order int `json:"order"`
}
type ClaimsProvider interface {
GetUsernameClaim() string
GetEmailClaim() string
GetNameClaim() string
}
func (p *OidcProvider) GetUsernameClaim() string {
return p.UsernameClaim
}
func (p *OidcProvider) GetEmailClaim() string {
return p.EmailClaim
}
func (p *OidcProvider) GetNameClaim() string {
return p.NameClaim
}

View File

@ -71,42 +71,6 @@ type oidcEndpoint struct {
Algorithms []string `json:"algorithms"`
}
type OidcProvider struct {
ClientID string `json:"client_id"`
ClientIDFile string `json:"client_id_file"`
ClientSecret string `json:"client_secret"`
ClientSecretFile string `json:"client_secret_file"`
RedirectURL string `json:"redirect_url"`
Scopes []string `json:"scopes"`
DisplayName string `json:"display_name"`
Color string `json:"color"`
Icon string `json:"icon"`
AutoDiscovery string `json:"provider_url"`
Endpoint oidcEndpoint `json:"endpoint"`
UsernameClaim string `json:"username_claim" default:"preferred_username"`
NameClaim string `json:"name_claim" default:"preferred_username"`
EmailClaim string `json:"email_claim" default:"email"`
Order int `json:"order"`
}
type ClaimsProvider interface {
GetUsernameClaim() string
GetEmailClaim() string
GetNameClaim() string
}
func (p *OidcProvider) GetUsernameClaim() string {
return p.UsernameClaim
}
func (p *OidcProvider) GetEmailClaim() string {
return p.EmailClaim
}
func (p *OidcProvider) GetNameClaim() string {
return p.NameClaim
}
const (
// GoGitClientId is builtin Git client. It is not require external dependencies and is preferred.
// Use it if you don't need external SSH authorization.
@ -143,17 +107,6 @@ type RunnerSettings struct {
MaxParallelTasks int `json:"max_parallel_tasks" default:"1" env:"SEMAPHORE_RUNNER_MAX_PARALLEL_TASKS"`
}
type AppVersion struct {
Semver string `json:"semver"`
DownloadURL string `json:"download_url"`
Path string `json:"path"`
}
type AppConfig struct {
Name string `json:"name"`
Versions []AppVersion `json:"versions"`
}
// ConfigType mapping between Config and the json file that sets it
type ConfigType struct {
MySQL DbConfig `json:"mysql"`
@ -238,9 +191,9 @@ type ConfigType struct {
Runner RunnerSettings `json:"runner"`
GlobalIntegrationAlias string `json:"global_integration_alias" env:"g"`
IntegrationAlias string `json:"global_integration_alias" env:"SEMAPHORE_INTEGRATION_ALIAS"`
Apps []AppConfig `json:"apps" env:"SEMAPHORE_APPS"`
Apps map[string]AppConfig `json:"apps" env:"SEMAPHORE_APPS"`
}
// Config exposes the application configuration storage for use in the application