diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d9ce3bd --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +FROM golang:1.13-alpine3.10 AS build-env + +#Setup +COPY . /src/gitea-group-sync +WORKDIR /src/gitea-group-sync + +#Build deps +RUN apk --no-cache add build-base git + +RUN go get gopkg.in/ldap.v3 && go get gopkg.in/robfig/cron.v3 && go build + +FROM alpine:3.10 + +COPY --from=build-env /src/gitea-group-sync/gitea-group-sync /app/gitea-group-sync/gitea-group-sync +RUN ln -s /app/gitea-group-sync/gitea-group-sync /usr/local/bin/gitea-group-sync +ENTRYPOINT ["/usr/local/bin/gitea-group-sync"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3184b8f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,16 @@ +version: '3' +services: + group-sync: + container_name: gitea-group-sync + build: . + image: localhost:5000/gitea-group-sync + environment: + GITEA_TOKEN: c00c810bb668c63ce7cd8057411d2f560eac469c + GITEA_URL: http://192.168.2.2:3000 + LDAP_URL: 192.168.2.2 + LDAP_TLS_PORT: 636 + BIND_DN: cn=admin,dc=planetexpress,dc=com + BIND_PASSWORD: GoodNewsEveryone + LDAP_FILTER: (&(objectClass=person)(memberOf=cn=%s,ou=people,dc=planetexpress,dc=com)) + LDAP_USER_SEARCH_BASE: 'ou=people,dc=planetexpress,dc=com' + REP_TIME: '@every 1m' diff --git a/gitea-group-sync.go b/gitea-group-sync.go new file mode 100644 index 0000000..24540e3 --- /dev/null +++ b/gitea-group-sync.go @@ -0,0 +1,234 @@ +package main + +import ( + "crypto/tls" + "fmt" + "log" + "net/url" + "os" + "strconv" + "strings" + "time" +) + +import "gopkg.in/ldap.v3" +import "gopkg.in/robfig/cron.v3" + +func AddUsersToTeam(apiKeys GiteaKeys, users []Account, team int) bool { + + for i := 0; i < len(users); i++ { + + fullusername := url.PathEscape(fmt.Sprintf("%s", users[i].Full_name)) + apiKeys.Command = "/api/v1/users/search?q=" + fullusername + "&access_token=" + foundUsers := RequestSearchResults(apiKeys) + + for j := 0; j < len(foundUsers.Data); j++ { + + if strings.EqualFold(users[i].Login, foundUsers.Data[j].Login) { + apiKeys.Command = "/api/v1/teams/" + fmt.Sprintf("%d", team) + "/members/" + foundUsers.Data[j].Login + "?access_token=" + error := RequestPut(apiKeys) + if len(error) > 0 { + log.Println("Error (Team does not exist or Not Found User) :", parseJson(error).(map[string]interface{})["message"]) + } + } + } + } + return true +} + +func DelUsersFromTeam(apiKeys GiteaKeys, Users []Account, team int) bool { + + for i := 0; i < len(Users); i++ { + + apiKeys.Command = "/api/v1/users/search?uid=" + fmt.Sprintf("%d", Users[i].Id) + "&access_token=" + + foundUser := RequestSearchResults(apiKeys) + + apiKeys.Command = "/api/v1/teams/" + fmt.Sprintf("%d", team) + "/members/" + foundUser.Data[0].Login + "?access_token=" + RequestDel(apiKeys) + } + return true +} + +func main() { + + mainJob() // First run for check settings + + var repTime string + if len(os.Getenv("REP_TIME")) == 0 { + + } else { + repTime = os.Getenv("REP_TIME") + } + + c := cron.New() + c.AddFunc(repTime, mainJob) + c.Start() + fmt.Println(c.Entries()) + for true { + time.Sleep(100*time.Second) + } +} + +func mainJob() { + + //------------------------------ + // Check and Set input settings + //------------------------------ + + var apiKeys GiteaKeys + + 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") + } + + 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 + if len(os.Getenv("LDAP_TLS_PORT")) > 0 { + port, err := strconv.Atoi(os.Getenv("LDAP_TLS_PORT")) + ldapPort = port + log.Printf("DialTLS:=%v:%d", ldapUrl, ldapPort) + if err != nil { + log.Println("LDAP_TLS_PORT is invalid.") + } + } else { + log.Println("LDAP_TLS_PORT is empty") + } + + var ldapbindDN string + if len(os.Getenv("BIND_DN")) == 0 { + log.Println("BIND_DN is empty") + } else { + ldapbindDN = os.Getenv("BIND_DN") + } + + var ldapbindPassword string + if len(os.Getenv("BIND_PASSWORD")) == 0 { + log.Println("BIND_PASSWORD is empty") + } else { + ldapbindPassword = os.Getenv("BIND_PASSWORD") + } + + var ldapUserFilter string + if len(os.Getenv("LDAP_FILTER")) == 0 { + log.Println("LDAP_FILTER is empty") + } else { + ldapUserFilter = os.Getenv("LDAP_FILTER") + } + + 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") + } + + l, err := ldap.DialTLS("tcp", fmt.Sprintf("%s:%d", ldapUrl, ldapPort), &tls.Config{InsecureSkipVerify: true}) + if err != nil { + fmt.Println(err) + fmt.Println("Please set the correct values for all specifics.") + os.Exit(1) + } + defer l.Close() + + err = l.Bind(ldapbindDN, 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) + + log.Printf("%d organizations were found on the server: %s", len(organizationList), apiKeys.BaseUrl) + + for 1 < len(organizationList) { + + for i := 0; i < len(organizationList); i++ { + + log.Println(organizationList) + + 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) + 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 + + for j := 1; j < len(teamList); j++ { + + // preparing request to ldap server + filter := fmt.Sprintf(ldapUserFilter, teamList[j].Name) + searchRequest := ldap.NewSearchRequest( + ldapUserSearchBase, // The base dn to search + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, + filter, // The filter to apply + []string{"cn", "uid", "mailPrimaryAddress, sn"}, // A list attributes to retrieve + nil, + ) + // make request to ldap server + sr, err := l.Search(searchRequest) + if err != nil { + log.Fatal(err) + } + AccountsLdap := make(map[string]Account) + 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) + for _, entry := range sr.Entries { + + AccountsLdap[entry.GetAttributeValue("uid")] = Account{ + Full_name: entry.GetAttributeValue("sn"), //change to cn if you need it + Login: entry.GetAttributeValue("uid"), + } + } + + 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) + + for k, v := range AccountsLdap { + if AccountsGitea[k].Login != v.Login { + addUserToTeamList = append(addUserToTeamList, v) + } + } + log.Printf("can be added users list %v", addUserToTeamList) + AddUsersToTeam(apiKeys, addUserToTeamList, teamList[j].Id) + + for k, v := range AccountsGitea { + if AccountsLdap[k].Login != v.Login { + delUserToTeamlist = append(delUserToTeamlist, v) + } + } + log.Printf("must be del users list %v", delUserToTeamlist) + DelUsersFromTeam(apiKeys, delUserToTeamlist, teamList[j].Id) + + } else { + log.Printf("The LDAP %s not found users corresponding to team %s", 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) + } +} diff --git a/images/Image3.png b/images/Image3.png new file mode 100644 index 0000000..bab63a0 Binary files /dev/null and b/images/Image3.png differ diff --git a/images/Image4.png b/images/Image4.png new file mode 100644 index 0000000..8f2f531 Binary files /dev/null and b/images/Image4.png differ diff --git a/images/Image5.png b/images/Image5.png new file mode 100644 index 0000000..c5ae617 Binary files /dev/null and b/images/Image5.png differ diff --git a/images/Image6.png b/images/Image6.png new file mode 100644 index 0000000..0a8efa4 Binary files /dev/null and b/images/Image6.png differ diff --git a/images/Image7.png b/images/Image7.png new file mode 100644 index 0000000..ef6e12d Binary files /dev/null and b/images/Image7.png differ diff --git a/images/Image8.png b/images/Image8.png new file mode 100644 index 0000000..1ddf763 Binary files /dev/null and b/images/Image8.png differ diff --git a/requests.go b/requests.go new file mode 100644 index 0000000..43e2139 --- /dev/null +++ b/requests.go @@ -0,0 +1,196 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + // "reflect" + "net" + "net/url" + "strings" + "time" +) + +func CheckStatusCode(res *http.Response) { + + switch { + case 300 <= res.StatusCode && res.StatusCode < 400: + fmt.Println("CheckStatusCode gitea apiKeys connection error: Redirect message") + case 401 == res.StatusCode: + fmt.Println("CheckStatusCode gitea apiKeys connection Error: Unauthorized") + case 400 <= res.StatusCode && res.StatusCode < 500: + fmt.Println("CheckStatusCode gitea apiKeys connection error: Client error") + case 500 <= res.StatusCode && res.StatusCode < 600: + fmt.Println("CheckStatusCode gitea apiKeys connection error Server error") + } +} + +func hasTimedOut(err error) bool { + switch err := err.(type) { + case *url.Error: + if err, ok := err.Err.(net.Error); ok && err.Timeout() { + return true + } + case net.Error: + if err.Timeout() { + return true + } + case *net.OpError: + if err.Timeout() { + return true + } + } + + errTxt := "use of closed network connection" + if err != nil && strings.Contains(err.Error(), errTxt) { + return true + } + + return false +} + +func RequestGet(apiKeys GiteaKeys) []byte { + cc := &http.Client{Timeout: time.Second * 2} + url := apiKeys.BaseUrl + apiKeys.Command + apiKeys.TokenKey[apiKeys.BruteforceTokenKey] + res, err := cc.Get(url) + if err != nil && hasTimedOut(err) { + fmt.Println(err) + os.Exit(1) + } + CheckStatusCode(res) + b, readErr := ioutil.ReadAll(res.Body) + if readErr != nil { + log.Fatal(readErr) + } + res.Body.Close() + return b +} + +func RequestPut(apiKeys GiteaKeys) []byte { + cc := &http.Client{Timeout: time.Second * 2} + url := apiKeys.BaseUrl + apiKeys.Command + apiKeys.TokenKey[apiKeys.BruteforceTokenKey] + request, err := http.NewRequest("PUT", url, nil) + res, err := cc.Do(request) + CheckStatusCode(res) + if err != nil && hasTimedOut(err) { + fmt.Println(err) + os.Exit(1) + } + b, readErr := ioutil.ReadAll(res.Body) + if readErr != nil { + log.Fatal(readErr) + } + res.Body.Close() + return b +} + +func RequestDel(apiKeys GiteaKeys) []byte { + + cc := &http.Client{Timeout: time.Second * 2} + url := apiKeys.BaseUrl + apiKeys.Command + apiKeys.TokenKey[apiKeys.BruteforceTokenKey] + request, err := http.NewRequest("DELETE", url, nil) + res, err := cc.Do(request) + CheckStatusCode(res) + if err != nil && hasTimedOut(err) { + fmt.Println(err) + os.Exit(1) + } + b, readErr := ioutil.ReadAll(res.Body) + if readErr != nil { + log.Fatal(readErr) + } + res.Body.Close() + return b +} + +func RequestSearchResults(ApiKeys GiteaKeys) SearchResults { + + b := RequestGet(ApiKeys) + + var f SearchResults + jsonErr := json.Unmarshal(b, &f) + if jsonErr != nil { + log.Fatal(jsonErr) + } + return f +} + +func RequestUsersList(ApiKeys GiteaKeys) (map[string]Account, int) { + + b := RequestGet(ApiKeys) + var Account_u = make(map[string]Account) + + var f []Account + jsonErr := json.Unmarshal(b, &f) + if jsonErr != nil { + log.Println(jsonErr) + if ApiKeys.BruteforceTokenKey == len(ApiKeys.TokenKey)-1 { + log.Println("Token key is unsuitable, call to system administrator ") + } else { + log.Println("Can't get UsersList try another token key") + } + if ApiKeys.BruteforceTokenKey < len(ApiKeys.TokenKey)-1 { + ApiKeys.BruteforceTokenKey++ + log.Printf("BruteforceTokenKey=%d", ApiKeys.BruteforceTokenKey) + Account_u, ApiKeys.BruteforceTokenKey = RequestUsersList(ApiKeys) + } + } + + for i := 0; i < len(f); i++ { + Account_u[f[i].Login] = Account{ + // Email: f[i].Email, + Id: f[i].Id, + Full_name: f[i].Full_name, + Login: f[i].Login, + } + } + return Account_u, ApiKeys.BruteforceTokenKey +} + +func RequestOrganizationList(apiKeys GiteaKeys) []Organization { + + b := RequestGet(apiKeys) + + var f []Organization + jsonErr := json.Unmarshal(b, &f) + if jsonErr != nil { + log.Printf("Please check setting GITEA_TOKEN, GITEA_URL ") + log.Fatal(jsonErr) + } + return f +} + +func RequestTeamList(apiKeys GiteaKeys) []Team { + + b := RequestGet(apiKeys) + + var f []Team + jsonErr := json.Unmarshal(b, &f) + if jsonErr != nil { + log.Fatal(jsonErr) + } + return f +} + +func parseJson(b []byte) interface{} { + var f interface{} + jsonErr := json.Unmarshal(b, &f) + if jsonErr != nil { + log.Fatal(jsonErr) + } + m := f.(interface{}) + return m +} + +func parseJsonArray(b []byte) []interface{} { + var f interface{} + jsonErr := json.Unmarshal(b, &f) + if jsonErr != nil { + log.Fatal(jsonErr) + } + m := f.([]interface{}) + return m +} diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..43b444c --- /dev/null +++ b/run.sh @@ -0,0 +1,11 @@ +#!/bin/sh +export GITEA_TOKEN=c00c810bb668c63ce7cd8057411d2f560eac469c,2c02df6959d012dee8f5da3539f63223417c4bbe +export GITEA_URL=http://localhost:3000 +export LDAP_URL=localhost +export LDAP_TLS_PORT=636 +export BIND_DN='cn=admin,dc=planetexpress,dc=com' +export BIND_PASSWORD=GoodNewsEveryone +export LDAP_FILTER='(&(objectClass=person)(memberOf=cn=%s,ou=people,dc=planetexpress,dc=com))' +export LDAP_USER_SEARCH_BASE='ou=people,dc=planetexpress,dc=com' +export REP_TIME='@every 1m' +go run . diff --git a/types.go b/types.go new file mode 100644 index 0000000..378aa27 --- /dev/null +++ b/types.go @@ -0,0 +1,48 @@ +package main + +type Organization struct { + Id int `json:"id"` + Avatar_url string `json:"avatar_url"` + Description string `json:"description"` + Full_name string `json:"full_name"` + Location string `json:"location"` + Name string `json:"username"` + Visibility string `json:"visibility"` + Website string `json:"website"` +} + +type Team struct { + Id int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Permission string `json:"permission"` +} +type User struct { + Id int `json:"id"` + Avatar_url string `json:"avatar_url"` + Created string `json:"created"` + Email string `json:"email"` + Full_name string `json:"full_name"` + Is_admin bool `json:"is_admin"` + Language string `json:"language"` + Last_login string `json:"last_login"` + Login string `json:"login"` +} + +type Account struct { + Id int `json:"id"` + Full_name string `json:"full_name"` + Login string `json:"login"` +} + +type SearchResults struct { + Data []User `json:"data"` + Ok bool `json:"ok"` +} + +type GiteaKeys struct { + TokenKey []string + BaseUrl string + Command string + BruteforceTokenKey int +}