diff --git a/README.md b/README.md index d5e63bc..2665521 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,15 @@ If you configured the [Gitea](https://hub.docker.com/r/gitea/gitea) <=> [LDAP](h You need to create Manage Access Tokens and add key to run.sh or docker-compose.yml the configuration file +##### Configuration: +There are two ways to configure the application. Via YAML Configuration File or Enviroment Variables. +- See `run.sh` for an example using the enviroment Variables. +- Use `./gitea-group-sync --config="config.yaml"` with the example Config File for the YAML Variant. + +##### Gitea Tokens The application supports several keys, since to add people to the group you must be the owner of the organization. + ![](images/Image2.png) #### create organizations in gitea diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..4773612 --- /dev/null +++ b/config.yaml @@ -0,0 +1,20 @@ +# Example Configuration for gitea-group-sync + +ApiKeys: + TokenKey: + - "c00c810bb668c63ce7cd8057411d2f560eac469c,2c02df6959d012dee8f5da3539f63223417c4bbe" + BaseUrl: "http://localhost:3200" + +# LDAP Config +LdapURL: "localhost" +LdapPort: 639 +LdapTLS: false +LdapBindDN: "cn=admin,dc=planetexpress,dc=com" +LdapBindPassword: "GoodNewsEveryone" +LdapFilter: '(&(objectClass=person)(memberOf=cn=%s,ou=people,dc=planetexpress,dc=com))' +LdapUserSearchBase: 'ou=people,dc=planetexpress,dc=com' +ReqTime: '@every 1m' +LdapUserIdentityAttribute: "uid" +LdapUserFullName: "sn" # can be changed to "cn" if needed + + diff --git a/gitea-group-sync.go b/gitea-group-sync.go index f1e3919..1fb2e2b 100644 --- a/gitea-group-sync.go +++ b/gitea-group-sync.go @@ -2,6 +2,7 @@ package main import ( "crypto/tls" + "flag" "fmt" "log" "net/url" @@ -9,10 +10,11 @@ import ( "strconv" "strings" "time" -) -import "gopkg.in/ldap.v3" -import "github.com/robfig/cron/v3" + "github.com/robfig/cron/v3" + "gopkg.in/ldap.v3" + "gopkg.in/yaml.v2" +) func AddUsersToTeam(apiKeys GiteaKeys, users []Account, team int) bool { @@ -50,8 +52,11 @@ func DelUsersFromTeam(apiKeys GiteaKeys, Users []Account, team int) bool { return true } -func main() { +var configFlag = flag.String("config", "config.yaml", "Specify YAML Configuration File") +func main() { + // Parse flags of programm + flag.Parse() mainJob() // First run for check settings var repTime string @@ -66,109 +71,148 @@ func main() { c.Start() fmt.Println(c.Entries()) for true { - time.Sleep(100*time.Second) + time.Sleep(100 * time.Second) } } -func mainJob() { +// This Function parses the enviroment for application specific variables and returns a Config struct. +// Used for setting all required settings in the application +func importEnvVars() Config { - //------------------------------ - // Check and Set input settings - //------------------------------ + // Create temporary structs for creating the final config + envConfig := Config{} - var apiKeys GiteaKeys + // ApiKeys + envConfig.ApiKeys = GiteaKeys{} + envConfig.ApiKeys.TokenKey = strings.Split(os.Getenv("GITEA_TOKEN"), ",") + envConfig.ApiKeys.BaseUrl = os.Getenv("GITEA_URL") - if len(os.Getenv("GITEA_TOKEN")) < 40 { // get on https://[web_site_url]/user/settings/applications - log.Println("GITEA_TOKEN is empty or invalid.") - } else { - apiKeys.TokenKey = strings.Split(os.Getenv("GITEA_TOKEN"), ",") - } - - if len(os.Getenv("GITEA_URL")) == 0 { - log.Println("GITEA_URL is empty") - } else { - apiKeys.BaseUrl = os.Getenv("GITEA_URL") - } + // LDAP Config + envConfig.LdapURL = os.Getenv("LDAP_URL") + envConfig.LdapBindDN = os.Getenv("BIND_DN") + envConfig.LdapBindPassword = os.Getenv("BIND_PASSWORD") + envConfig.LdapFilter = os.Getenv("LDAP_FILTER") + envConfig.LdapUserSearchBase = os.Getenv("LDAP_USER_SEARCH_BASE") - var ldapUrl string = "ucs.totalwebservices.net" - if len(os.Getenv("LDAP_URL")) == 0 { - log.Println("LDAP_URL is empty") - } else { - ldapUrl = os.Getenv("LDAP_URL") - } - - var ldapPort int - var ldapTls bool + // Check TLS Settings if len(os.Getenv("LDAP_TLS_PORT")) > 0 { port, err := strconv.Atoi(os.Getenv("LDAP_TLS_PORT")) - ldapPort = port - ldapTls = true - log.Printf("DialTLS:=%v:%d", ldapUrl, ldapPort) + envConfig.LdapPort = port + envConfig.LdapTLS = true + log.Printf("DialTLS:=%v:%d", envConfig.LdapURL, envConfig.LdapPort) if err != nil { log.Println("LDAP_TLS_PORT is invalid.") } } else { if len(os.Getenv("LDAP_PORT")) > 0 { port, err := strconv.Atoi(os.Getenv("LDAP_PORT")) - ldapPort = port - ldapTls = false - log.Printf("Dial:=%v:%d", ldapUrl, ldapPort) + envConfig.LdapPort = port + envConfig.LdapTLS = false + log.Printf("Dial:=%v:%d", envConfig.LdapURL, envConfig.LdapPort) if err != nil { log.Println("LDAP_PORT is invalid.") } } -} - - var ldapbindDN string - if len(os.Getenv("BIND_DN")) == 0 { - log.Println("BIND_DN is empty") + } + // Set defaults for user Attributes + if len(os.Getenv("LDAP_USER_IDENTITY_ATTRIBUTE")) == 0 { + envConfig.LdapUserIdentityAttribute = "uid" + log.Println("By default LDAP_USER_IDENTITY_ATTRIBUTE = 'uid'") } else { - ldapbindDN = os.Getenv("BIND_DN") + envConfig.LdapUserIdentityAttribute = os.Getenv("LDAP_USER_IDENTITY_ATTRIBUTE") } - var ldapbindPassword string - if len(os.Getenv("BIND_PASSWORD")) == 0 { - log.Println("BIND_PASSWORD is empty") + if len(os.Getenv("LDAP_USER_FULL_NAME")) == 0 { + envConfig.LdapUserFullName = "sn" //change to cn if you need it + log.Println("By default LDAP_USER_FULL_NAME = 'sn'") } else { - ldapbindPassword = os.Getenv("BIND_PASSWORD") + envConfig.LdapUserFullName = os.Getenv("LDAP_USER_FULL_NAME") } - var ldapUserFilter string - if len(os.Getenv("LDAP_FILTER")) == 0 { - log.Println("LDAP_FILTER is empty") - } else { - ldapUserFilter = os.Getenv("LDAP_FILTER") + return envConfig // return the config struct for use. +} + +func importYAMLConfig(path string) (Config, error) { + // Open Config File + f, err := os.Open(path) + if err != nil { + return Config{}, err // Aborting } + defer f.Close() - var ldapUserSearchBase string - if len(os.Getenv("LDAP_USER_SEARCH_BASE")) == 0 { - log.Println("LDAP_USER_SEARCH_BASE is empty") - } else { - ldapUserSearchBase = os.Getenv("LDAP_USER_SEARCH_BASE") + // Parse File into Config Struct + var cfg Config + decoder := yaml.NewDecoder(f) + err = decoder.Decode(&cfg) + if err != nil { + return Config{}, err // Aborting } + return cfg, nil +} - var ldapUserIdentityAttribute string - if len(os.Getenv("LDAP_USER_IDENTITY_ATTRIBUTE")) == 0 { - ldapUserIdentityAttribute = "uid" - log.Println("By default LDAP_USER_IDENTITY_ATTRIBUTE = 'uid'") +func (c Config) checkConfig() { + if len(c.ApiKeys.TokenKey) <= 0 { + log.Println("GITEA_TOKEN is empty or invalid.") + } + if len(c.ApiKeys.BaseUrl) == 0 { + log.Println("GITEA_URL is empty") + } + if len(c.LdapURL) == 0 { + log.Println("LDAP_URL is empty") + } + if c.LdapPort <= 0 { + log.Println("LDAP_TLS_PORT is invalid.") } else { - ldapUserIdentityAttribute = os.Getenv("LDAP_USER_IDENTITY_ATTRIBUTE") + log.Printf("DialTLS:=%v:%d", c.LdapURL, c.LdapPort) } - - var ldapUserFullName string - if len(os.Getenv("LDAP_USER_FULL_NAME")) == 0 { - ldapUserFullName = "sn" //change to cn if you need it + if len(c.LdapBindDN) == 0 { + log.Println("BIND_DN is empty") + } + if len(c.LdapBindPassword) == 0 { + log.Println("BIND_PASSWORD is empty") + } + if len(c.LdapFilter) == 0 { + log.Println("LDAP_FILTER is empty") + } + if len(c.LdapUserSearchBase) == 0 { + log.Println("LDAP_USER_SEARCH_BASE is empty") + } + if len(c.LdapUserIdentityAttribute) == 0 { + c.LdapUserIdentityAttribute = "uid" + log.Println("By default LDAP_USER_IDENTITY_ATTRIBUTE = 'uid'") + } + if len(c.LdapUserFullName) == 0 { + c.LdapUserFullName = "sn" //change to cn if you need it log.Println("By default LDAP_USER_FULL_NAME = 'sn'") + } +} + +func mainJob() { + + //------------------------------ + // Check and Set input settings + //------------------------------ + var cfg Config + + cfg, importErr := importYAMLConfig(*configFlag) + if importErr != nil { + log.Println("Fallback: Importing Settings from Enviroment Variables ") + cfg = importEnvVars() } else { - ldapUserFullName = os.Getenv("LDAP_USER_FULL_NAME") + log.Println("Successfully imported YAML Config from " + *configFlag) + fmt.Println(cfg) } + // Checks Config + cfg.checkConfig() + log.Println("Checked config elements") + // Prepare LDAP Connection var l *ldap.Conn var err error - if ldapTls { - l, err = ldap.DialTLS("tcp", fmt.Sprintf("%s:%d", ldapUrl, ldapPort), &tls.Config{InsecureSkipVerify: true}) + if cfg.LdapTLS { + l, err = ldap.DialTLS("tcp", fmt.Sprintf("%s:%d", cfg.LdapURL, cfg.LdapPort), &tls.Config{InsecureSkipVerify: true}) } else { - l, err = ldap.Dial("tcp", fmt.Sprintf("%s:%d", ldapUrl, ldapPort)) + l, err = ldap.Dial("tcp", fmt.Sprintf("%s:%d", cfg.LdapURL, cfg.LdapPort)) } if err != nil { @@ -178,16 +222,16 @@ func mainJob() { } defer l.Close() - err = l.Bind(ldapbindDN, ldapbindPassword) + err = l.Bind(cfg.LdapBindDN, cfg.LdapBindPassword) if err != nil { log.Fatal(err) } page := 1 - apiKeys.BruteforceTokenKey = 0 - apiKeys.Command = "/api/v1/admin/orgs?page=" + fmt.Sprintf("%d", page) + "&limit=20&access_token=" // List all organizations - organizationList := RequestOrganizationList(apiKeys) + cfg.ApiKeys.BruteforceTokenKey = 0 + cfg.ApiKeys.Command = "/api/v1/admin/orgs?page=" + fmt.Sprintf("%d", page) + "&limit=20&access_token=" // List all organizations + organizationList := RequestOrganizationList(cfg.ApiKeys) - log.Printf("%d organizations were found on the server: %s", len(organizationList), apiKeys.BaseUrl) + log.Printf("%d organizations were found on the server: %s", len(organizationList), cfg.ApiKeys.BaseUrl) for 1 < len(organizationList) { @@ -197,21 +241,21 @@ func mainJob() { log.Printf("Begin an organization review: OrganizationName= %v, OrganizationId= %d \n", organizationList[i].Name, organizationList[i].Id) - apiKeys.Command = "/api/v1/orgs/" + organizationList[i].Name + "/teams?access_token=" - teamList := RequestTeamList(apiKeys) + cfg.ApiKeys.Command = "/api/v1/orgs/" + organizationList[i].Name + "/teams?access_token=" + teamList := RequestTeamList(cfg.ApiKeys) log.Printf("%d teams were found in %s organization", len(teamList), organizationList[i].Name) log.Printf("Skip synchronization in the Owners team") - apiKeys.BruteforceTokenKey = 0 + cfg.ApiKeys.BruteforceTokenKey = 0 for j := 1; j < len(teamList); j++ { // preparing request to ldap server - filter := fmt.Sprintf(ldapUserFilter, teamList[j].Name) + filter := fmt.Sprintf(cfg.LdapFilter, teamList[j].Name) searchRequest := ldap.NewSearchRequest( - ldapUserSearchBase, // The base dn to search + cfg.LdapUserSearchBase, // The base dn to search ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, filter, // The filter to apply - []string{"cn", "uid", "mailPrimaryAddress, sn", ldapUserIdentityAttribute}, // A list attributes to retrieve + []string{"cn", "uid", "mailPrimaryAddress, sn", cfg.LdapUserIdentityAttribute}, // A list attributes to retrieve nil, ) // make request to ldap server @@ -223,18 +267,18 @@ func mainJob() { AccountsGitea := make(map[string]Account) var addUserToTeamList, delUserToTeamlist []Account if len(sr.Entries) > 0 { - log.Printf("The LDAP %s has %d users corresponding to team %s", ldapUrl, len(sr.Entries), teamList[j].Name) + log.Printf("The LDAP %s has %d users corresponding to team %s", cfg.LdapURL, len(sr.Entries), teamList[j].Name) for _, entry := range sr.Entries { - AccountsLdap[entry.GetAttributeValue(ldapUserIdentityAttribute)] = Account{ - Full_name: entry.GetAttributeValue(ldapUserFullName), - Login: entry.GetAttributeValue(ldapUserIdentityAttribute), + AccountsLdap[entry.GetAttributeValue(cfg.LdapUserIdentityAttribute)] = Account{ + Full_name: entry.GetAttributeValue(cfg.LdapUserFullName), + Login: entry.GetAttributeValue(cfg.LdapUserIdentityAttribute), } } - apiKeys.Command = "/api/v1/teams/" + fmt.Sprintf("%d", teamList[j].Id) + "/members?access_token=" - AccountsGitea, apiKeys.BruteforceTokenKey = RequestUsersList(apiKeys) - log.Printf("The gitea %s has %d users corresponding to team %s Teamid=%d", apiKeys.BaseUrl, len(AccountsGitea), teamList[j].Name, teamList[j].Id) + cfg.ApiKeys.Command = "/api/v1/teams/" + fmt.Sprintf("%d", teamList[j].Id) + "/members?access_token=" + AccountsGitea, cfg.ApiKeys.BruteforceTokenKey = RequestUsersList(cfg.ApiKeys) + log.Printf("The gitea %s has %d users corresponding to team %s Teamid=%d", cfg.ApiKeys.BaseUrl, len(AccountsGitea), teamList[j].Name, teamList[j].Id) for k, v := range AccountsLdap { if AccountsGitea[k].Login != v.Login { @@ -242,7 +286,7 @@ func mainJob() { } } log.Printf("can be added users list %v", addUserToTeamList) - AddUsersToTeam(apiKeys, addUserToTeamList, teamList[j].Id) + AddUsersToTeam(cfg.ApiKeys, addUserToTeamList, teamList[j].Id) for k, v := range AccountsGitea { if AccountsLdap[k].Login != v.Login { @@ -250,18 +294,18 @@ func mainJob() { } } log.Printf("must be del users list %v", delUserToTeamlist) - DelUsersFromTeam(apiKeys, delUserToTeamlist, teamList[j].Id) + DelUsersFromTeam(cfg.ApiKeys, delUserToTeamlist, teamList[j].Id) } else { - log.Printf("The LDAP %s not found users corresponding to team %s", ldapUrl, teamList[j].Name) + log.Printf("The LDAP %s not found users corresponding to team %s", cfg.LdapURL, teamList[j].Name) } } } page++ - apiKeys.BruteforceTokenKey = 0 - apiKeys.Command = "/api/v1/admin/orgs?page=" + fmt.Sprintf("%d", page) + "&limit=20&access_token=" // List all organizations - organizationList = RequestOrganizationList(apiKeys) - log.Printf("%d organizations were found on the server: %s", len(organizationList), apiKeys.BaseUrl) + cfg.ApiKeys.BruteforceTokenKey = 0 + cfg.ApiKeys.Command = "/api/v1/admin/orgs?page=" + fmt.Sprintf("%d", page) + "&limit=20&access_token=" // List all organizations + organizationList = RequestOrganizationList(cfg.ApiKeys) + log.Printf("%d organizations were found on the server: %s", len(organizationList), cfg.ApiKeys.BaseUrl) } } diff --git a/go.mod b/go.mod index a49b0e6..78bcb52 100644 --- a/go.mod +++ b/go.mod @@ -5,4 +5,5 @@ go 1.14 require ( github.com/robfig/cron/v3 v3.0.1 gopkg.in/ldap.v3 v3.1.0 + gopkg.in/yaml.v2 v2.3.0 ) diff --git a/types.go b/types.go index 378aa27..1e90feb 100644 --- a/types.go +++ b/types.go @@ -41,8 +41,23 @@ type SearchResults struct { } type GiteaKeys struct { - TokenKey []string - BaseUrl string + TokenKey []string `yaml:"TokenKey"` + BaseUrl string `yaml:"BaseUrl"` Command string BruteforceTokenKey int } + +// Config describes the settings of the application. This structure is used in the settings-import process +type Config struct { + ApiKeys GiteaKeys `yaml:"ApiKeys"` + LdapURL string `yaml:"LdapURL"` + LdapPort int `yaml:"LdapPort"` + LdapTLS bool `yaml:"LdapTLS"` + LdapBindDN string `yaml:"LdapBindDN"` + LdapBindPassword string `yaml:"LdapBindPassword"` + LdapFilter string `yaml:"LdapFilter"` + LdapUserSearchBase string `yaml:"LdapUserSearchBase"` + ReqTime string `yaml:"ReqTime"` + LdapUserIdentityAttribute string `yaml:"LdapUserIdentityAttribute"` + LdapUserFullName string `yaml:"LdapUserFullName"` +} //!TODO! Implement check if valid