mirror of
https://github.com/semaphoreui/semaphore.git
synced 2024-11-23 20:35:24 +01:00
feat: load options from db
This commit is contained in:
parent
b0e766355a
commit
7195913a5f
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
|
22
api/user.go
22
api/user.go
@ -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)
|
||||
}
|
||||
|
||||
|
@ -108,5 +108,11 @@ func createStore(token string) db.Store {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = db.FillConfigFromDB(store)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return store
|
||||
}
|
||||
|
25
db/Store.go
25
db/Store.go
@ -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
|
||||
}
|
||||
|
@ -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
99
db/config.go
Normal 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
40
db/config_test.go
Normal 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)
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
9
util/App.go
Normal 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
37
util/OdbcProvider.go
Normal 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
|
||||
}
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user