init commit

This commit is contained in:
Игорь Грищенко 2026-03-16 18:06:59 +03:00
commit 3ccb7519ef
17 changed files with 627 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*.log

2
Makefile Normal file
View File

@ -0,0 +1,2 @@
run_usersUpdateLoginMethod:
go run cmd/main.go -c usersUpdateLoginMethod

0
README.md Normal file
View File

49
cmd/main.go Normal file
View File

@ -0,0 +1,49 @@
package main
import (
"flag"
"scripts/internal/command"
"scripts/internal/config"
e "scripts/pkg/errors"
"scripts/pkg/logger"
l "scripts/pkg/logger"
)
const defaultConfigPath = "config.json"
func main() {
var (
commandName string
confPath string
)
flag.StringVar(&confPath, "config", defaultConfigPath, "path to config file")
flag.StringVar(&commandName, "c", "", "command name")
flag.Parse()
config.MustLoad(confPath)
if commandName == "" {
l.Log.Fatal().Msg("flag '-c' required")
}
logger.MustInit()
command.InitSripts()
cmd, exists := command.Scripts[commandName]
if !exists {
l.Log.Warn().Err(e.ErrCommandNotFound).Str("command", commandName).Send()
l.Log.Info().Msg("selected command - 'help'")
if err := command.Scripts["help"].Exec(); err != nil {
l.Log.Fatal().Err(err).Str("command", commandName).Msg("failed to use help command")
}
return
}
l.Log.Info().Msgf("selected command - %s", commandName)
if err := cmd.Exec(); err != nil {
l.Log.Error().Err(err).Str("command", commandName).Msg("failed to execude command")
return
}
}

43
config.json Normal file
View File

@ -0,0 +1,43 @@
{
"SqlSettings": {
"DriverName": "postgres",
"DataSource": "postgres://mmuser:mostest@localhost/mattermost_test?sslmode=disable\u0026connect_timeout=10\u0026binary_parameters=yes",
"DataSourceReplicas": [],
"DataSourceSearchReplicas": [],
"MaxIdleConns": 20,
"ConnMaxLifetimeMilliseconds": 3600000,
"ConnMaxIdleTimeMilliseconds": 300000,
"MaxOpenConns": 300,
"Trace": false,
"AtRestEncryptKey": "cirgp8x5eydrr9sjmwawb5zsefhbditq",
"QueryTimeout": 30,
"DisableDatabaseSearch": false,
"MigrationsStatementTimeoutSeconds": 100000,
"ReplicaLagSettings": [],
"ReplicaMonitorIntervalSeconds": 5
},
"LogSettings": {
"EnableConsole": true,
"ConsoleLevel": "DEBUG",
"ConsoleJson": true,
"EnableColor": false,
"EnableFile": true,
"FileLevel": "INFO",
"FileJson": true,
"FileLocation": "./pkg/logger/logs.log",
"EnableWebhookDebugging": true,
"EnableDiagnostics": true,
"VerboseDiagnostics": false,
"EnableSentry": true,
"EnableCaller": true,
"AdvancedLoggingJSON": {},
"AdvancedLoggingConfig": "",
"MaxFieldSize": 2048
},
"KeycloakSettings": {
"address": "http://localhost:8282",
"realm_name": "loop",
"admin_name": "admin",
"admin_pass": "admin"
}
}

15
go.mod Normal file
View File

@ -0,0 +1,15 @@
module scripts
go 1.26.0
require (
github.com/jmoiron/sqlx v1.4.0
github.com/lib/pq v1.10.9
github.com/rs/zerolog v1.34.0
)
require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
golang.org/x/sys v0.12.0 // indirect
)

25
go.sum Normal file
View File

@ -0,0 +1,25 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

View File

@ -0,0 +1,48 @@
package command
import (
"fmt"
commandEmailPassToOidc "scripts/internal/command/email_pass_to_oidc"
)
func InitSripts() {
scripts := map[string]*Commands{
commandEmailPassToOidc.Name: {
Exec: commandEmailPassToOidc.UsersUpdateLoginMethod,
Requirements: commandEmailPassToOidc.Requirement,
Description: "переводит авторизацию пользователей с ролью 'system_user' c почта/пароль на keycloak",
},
"help": {
Exec: Help,
Requirements: Requirement,
Description: "вывод всех доступных скриптов",
},
}
Scripts = scripts
}
var Scripts map[string]*Commands
type Commands struct {
Exec Execute
Requirements Requirements
Description string
}
type Execute func() error
type Requirements func() string
func Help() error {
fmt.Printf("All commands:\n\n")
for k, v := range Scripts {
fmt.Printf("name: %s\n", k)
fmt.Printf("description: %s\n", v.Description)
fmt.Printf("requirements: %s\n\n", v.Requirements())
}
return nil
}
func Requirement() string {
return "none"
}

View File

@ -0,0 +1,32 @@
package commandEmailPassToOidc
type KeycloakUser struct {
ID string `json:"id"`
Username string `json:"username"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Email string `json:"email"`
EmailVerified bool `json:"emailVerified"`
Attributes map[string][]string `json:"attributes"`
Enabled bool `json:"enabled"`
}
type KeycloakTokenResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
}
type LoopUser struct {
Id string `json:"id" db:"id"`
Username string `json:"username" db:"username"`
Password string `json:"password,omitempty" db:"password"`
AuthData *string `json:"auth_data,omitempty" db:"authdata"`
AuthService *string `json:"auth_service" db:"authservice"`
Email string `json:"email" db:"email"`
EmailVerified bool `json:"email_verified,omitempty" db:"emailverified"`
Nickname string `json:"nickname" db:"nickname"`
FirstName string `json:"first_name" db:"firstname"`
LastName string `json:"last_name" db:"lastname"`
Roles string `json:"roles" db:"roles"`
}

View File

@ -0,0 +1,154 @@
package commandEmailPassToOidc
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
c "scripts/internal/config"
l "scripts/pkg/logger"
"scripts/pkg/postgres"
)
const Name = "usersUpdateLoginMethod"
func UsersUpdateLoginMethod() error {
postgres.InitDB()
defer postgres.DB.Close()
tokenData, err := keycloakAccessToken()
if err != nil {
l.Log.Error().Err(err).Msg("failed to get access token from keycloak")
return err
}
loopUsers, err := allLoopUsers()
if err != nil {
l.Log.Error().Err(err).Msg("failed to get users from loop db")
return err
}
if len(loopUsers) == 0 {
l.Log.Info().Msg("no users in loop db for change login")
return nil
}
keycloakUsers, err := allKeycloakUsers(tokenData.AccessToken)
if err != nil {
l.Log.Error().Err(err).Msg("failed to get users from kecloak")
return err
}
if err := changeLogin(loopUsers, keycloakUsers); err != nil {
return err
}
l.Log.Info().Str("name", Name).Msg("command successfully")
return nil
}
func keycloakAccessToken() (*KeycloakTokenResponse, error) {
url := fmt.Sprintf("%s/realms/%s/protocol/openid-connect/token", c.Conf.KeycloakSettings.Address, "master")
var data bytes.Buffer
fmt.Fprintf(&data, "grant_type=password&client_id=admin-cli&username=%s&password=%s", c.Conf.KeycloakSettings.AdminName, c.Conf.KeycloakSettings.AdminPass)
req, err := http.NewRequest("POST", url, &data)
if err != nil {
l.Log.Error().Err(err).Msg("failed to create http request")
return nil, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
client := http.Client{}
resp, err := client.Do(req)
if err != nil {
l.Log.Error().Err(err).Msg("failed to exec http request")
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to get access token, status %d", resp.StatusCode)
}
var tokenResp KeycloakTokenResponse
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
l.Log.Error().Err(err).Msg("failed to decode body response")
return nil, err
}
return &tokenResp, nil
}
func allLoopUsers() (map[string]LoopUser, error) {
ctx := postgres.DB.Context()
var users []LoopUser
if err := postgres.DB.Conn().SelectContext(ctx, &users, allUsersQuery); err != nil {
l.Log.Error().Err(err).Msg("failed to select query")
return nil, err
}
result := make(map[string]LoopUser, len(users))
for _, el := range users {
result[el.Email] = el
}
return result, nil
}
func allKeycloakUsers(accessToken string) ([]KeycloakUser, error) {
url := fmt.Sprintf("%s/admin/realms/%s/users", c.Conf.KeycloakSettings.Address, c.Conf.KeycloakSettings.RealmName)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
l.Log.Error().Err(err).Msg("failed to create http request")
return nil, err
}
req.Header.Set("Authorization", "Bearer "+accessToken)
client := http.Client{}
resp, err := client.Do(req)
if err != nil {
l.Log.Error().Err(err).Msg("failed to exec http request")
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to get users: status %d", resp.StatusCode)
}
var users []KeycloakUser
if err := json.NewDecoder(resp.Body).Decode(&users); err != nil {
l.Log.Error().Err(err).Msg("failed to decode body response")
return nil, err
}
return users, nil
}
func changeLogin(loopUsers map[string]LoopUser, keycloakUsers []KeycloakUser) error {
for _, el := range keycloakUsers {
user, exists := loopUsers[el.Email]
if !exists {
l.Log.Warn().Str("email", el.Email).Msg("user not found")
continue
}
// NOTE: если в keycloak не были указаны firstname и lastname - при первои входе сам keycloak спросит их и ЗАМЕНИТ данные в БД loop
if _, err := postgres.DB.Conn().ExecContext(postgres.DB.Context(), changeLoginQuery, user.Id, el.ID, el.EmailVerified); err != nil {
l.Log.Error().Err(err).Msg("failed to exec update query")
return err
}
l.Log.Info().Str("email", el.Email).Msg("successfully update")
}
return nil
}

View File

@ -0,0 +1,34 @@
package commandEmailPassToOidc
const (
allUsersQuery = `
SELECT
users.id,
users.username,
users.password,
users.authdata,
users.authservice,
users.email,
users.emailverified,
users.nickname,
users.firstname,
users.lastname,
users.roles
FROM users
LEFT JOIN bots ON users.id = bots.userid
WHERE users.deleteat = 0
AND bots.userid IS NULL
AND roles LIKE '%system_user%'
AND roles NOT LIKE '%system_admin%'
AND (users.authservice != 'openid' OR users.authservice IS NULL)
`
changeLoginQuery = `
UPDATE users
SET password = '',
authdata = $2,
emailverified = $3,
authservice = 'openid'
WHERE id = $1
`
)

View File

@ -0,0 +1,20 @@
package commandEmailPassToOidc
import (
"reflect"
c "scripts/internal/config"
"strings"
)
func Requirement() string {
var required []string
t := reflect.TypeFor[c.KeycloakSettings]()
for field := range t.Fields() {
jsonTag := field.Tag.Get("json")
required = append(required, jsonTag)
}
return "необходимо задать в конфигурации ключ KeycloakSettings с ключами в объекте: " + strings.Join(required, "; ")
}

67
internal/config/config.go Normal file
View File

@ -0,0 +1,67 @@
package config
import (
"encoding/json"
"log"
"os"
)
type SqlSettings struct {
DriverName string `json:"DriverName"`
DataSource string `json:"DataSource"`
DataSourceReplicas []string `json:"DataSourceReplicas"`
DataSourceSearchReplicas []string `json:"DataSourceSearchReplicas"`
MaxIdleConns int `json:"MaxIdleConns"`
ConnMaxLifetimeMilliseconds int64 `json:"ConnMaxLifetimeMilliseconds"`
ConnMaxIdleTimeMilliseconds int64 `json:"ConnMaxIdleTimeMilliseconds"`
MaxOpenConns int `json:"MaxOpenConns"`
Trace bool `json:"Trace"`
AtRestEncryptKey string `json:"AtRestEncryptKey"`
QueryTimeout int `json:"QueryTimeout"`
DisableDatabaseSearch bool `json:"DisableDatabaseSearch"`
MigrationsStatementTimeoutSeconds int `json:"MigrationsStatementTimeoutSeconds"`
ReplicaMonitorIntervalSeconds int `json:"ReplicaMonitorIntervalSeconds"`
// ReplicaLagSettings []ReplicaLagSetting `json:"ReplicaLagSettings"`
}
type LogSettings struct {
EnableConsole bool `json:"EnableConsole"`
ConsoleLevel string `json:"ConsoleLevel"`
ConsoleJson bool `json:"ConsoleJson"`
EnableColor bool `json:"EnableColor"`
EnableFile bool `json:"EnableFile"`
FileLevel string `json:"FileLevel"`
FileJson bool `json:"FileJson"`
FileLocation string `json:"FileLocation"`
EnableWebhookDebugging bool `json:"EnableWebhookDebugging"`
EnableDiagnostics bool `json:"EnableDiagnostics"`
VerboseDiagnostics bool `json:"VerboseDiagnostics"`
EnableSentry bool `json:"EnableSentry"`
EnableCaller bool `json:"EnableCaller"`
AdvancedLoggingConfig string `json:"AdvancedLoggingConfig"`
MaxFieldSize int `json:"MaxFieldSize"`
// AdvancedLoggingJSON map[string]interface{} `json:"AdvancedLoggingJSON"`
}
var Conf Config
type Config struct {
SqlSettings SqlSettings `json:"SqlSettings"`
LogSettings LogSettings `json:"LogSettings"`
KeycloakSettings KeycloakSettings `json:"KeycloakSettings"`
}
func MustLoad(confPath string) {
file, err := os.Open(confPath)
if err != nil {
log.Fatal("err", "failed to open file")
}
defer file.Close()
decoder := json.NewDecoder(file)
if err := decoder.Decode(&Conf); err != nil {
log.Fatal(err, "failed to decode")
}
log.Println("init config successfully")
}

View File

@ -0,0 +1,8 @@
package config
type KeycloakSettings struct {
Address string `json:"address"`
RealmName string `json:"realm_name"`
AdminName string `json:"admin_name"`
AdminPass string `json:"admin_pass"`
}

7
pkg/errors/errors.go Normal file
View File

@ -0,0 +1,7 @@
package errors
import "errors"
var (
ErrCommandNotFound = errors.New("command not found")
)

54
pkg/logger/logger.go Normal file
View File

@ -0,0 +1,54 @@
package logger
import (
"io"
"log"
"os"
c "scripts/internal/config"
"github.com/rs/zerolog"
)
const timeFormat = "2006-01-02 15:04:05"
var Log zerolog.Logger
func MustInit() {
writers := make([]io.Writer, 0, 2)
zerolog.TimeFieldFormat = timeFormat
consoleWriter := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: timeFormat}
writers = append(writers, consoleWriter)
if c.Conf.LogSettings.EnableFile {
logFile, err := os.OpenFile(c.Conf.LogSettings.FileLocation, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
log.Fatal(err)
}
writers = append(writers, logFile)
}
multi := zerolog.MultiLevelWriter(writers...)
zCtx := zerolog.New(multi).Level(detectedLogLevel(c.Conf.LogSettings.ConsoleLevel)).With().Timestamp()
if c.Conf.LogSettings.EnableCaller {
zCtx = zCtx.Caller()
}
Log = zCtx.Logger()
}
func detectedLogLevel(level string) zerolog.Level {
switch level {
case "TRACE":
return zerolog.TraceLevel
case "DEBUG":
return zerolog.DebugLevel
case "WARN":
return zerolog.WarnLevel
case "INFO":
return zerolog.InfoLevel
default:
return zerolog.InfoLevel
}
}

68
pkg/postgres/connect.go Normal file
View File

@ -0,0 +1,68 @@
package postgres
import (
"context"
c "scripts/internal/config"
l "scripts/pkg/logger"
"time"
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
)
var DB *Postgres
type Option func(*Postgres)
type Postgres struct {
ctx context.Context
conn *sqlx.DB
}
func (p *Postgres) Conn() *sqlx.DB {
return p.conn
}
func (p *Postgres) Context() context.Context {
return p.ctx
}
func WithContext(ctx context.Context) Option {
return func(p *Postgres) {
p.ctx = ctx
}
}
func InitDB(opts ...Option) {
p := &Postgres{
ctx: context.Background(),
}
for _, opt := range opts {
opt(p)
}
db, err := sqlx.ConnectContext(p.ctx, c.Conf.SqlSettings.DriverName, c.Conf.SqlSettings.DataSource)
if err != nil {
l.Log.Fatal().Err(err).Msg("failed to connect DB")
}
db.SetMaxIdleConns(c.Conf.SqlSettings.MaxIdleConns)
db.SetMaxOpenConns(c.Conf.SqlSettings.MaxOpenConns)
db.SetConnMaxLifetime(time.Duration(c.Conf.SqlSettings.ConnMaxLifetimeMilliseconds) * time.Millisecond)
db.SetConnMaxIdleTime(time.Duration(c.Conf.SqlSettings.ConnMaxIdleTimeMilliseconds) * time.Millisecond)
if err := db.Ping(); err != nil {
l.Log.Fatal().Err(err).Msg("failed to ping DB")
}
p.conn = db
DB = p
l.Log.Info().Msg("connect to postgres successfully ")
}
func (p *Postgres) Close() {
if err := p.Conn().Close(); err != nil {
l.Log.Error().Err(err).Msg("failed to close connect DB")
}
}