diff --git a/Dockerfile b/Dockerfile index 50d8c89..aef30af 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Dockerfile for goldapps production -FROM golang:alpine AS buildStage +FROM golang:1.12-alpine AS buildStage MAINTAINER digIT # Install git @@ -8,16 +8,16 @@ RUN apk upgrade RUN apk add --update git # Copy sources -RUN mkdir -p $GOPATH/src/github.com/cthit/goldapps -COPY . $GOPATH/src/github.com/cthit/goldapps -WORKDIR $GOPATH/src/github.com/cthit/goldapps/cmd +RUN mkdir -p /goldapps +COPY . /goldapps +WORKDIR /goldapps/cmd/goldapps # Grab dependencies -RUN go get -d -v ./... +#RUN go get -d -v ./... # build binary RUN go install -v -RUN mkdir /app && mv $GOPATH/bin/cmd /app/goldapps +RUN mkdir /app && mv $GOPATH/bin/goldapps /app/goldapps ########################## # PRODUCTION STAGE # @@ -25,15 +25,23 @@ RUN mkdir /app && mv $GOPATH/bin/cmd /app/goldapps FROM alpine MAINTAINER digIT +# Add standard certificates +RUN apk update && apk add ca-certificates && rm -rf /var/cache/apk/* + # Set user RUN addgroup -S app RUN adduser -S -G app -s /bin/bash app USER app:app +# Copy execution script +COPY ./sleep_and_run.sh /app/sleep_and_run.sh + # Copy binary COPY --from=buildStage /app/goldapps /app/goldapps +ENV WAIT 15s + # Set good defaults WORKDIR /app -ENTRYPOINT /app/goldapps +ENTRYPOINT ./sleep_and_run.sh CMD -dry \ No newline at end of file diff --git a/README.md b/README.md index 8e85c12..8a926a1 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,18 @@ A system for syncing LDAP with gsuite and json files written in Go * use the downloaded file ## Usage -[docker image](https://hub.docker.com/r/cthit/goldapps/) + +Read setup first + +### Docker image + +`$WAIT` specifies for how long the application should wait before running. This can bes jused in conjunction with `restart: always` to make the bridge run at regular intervals. If you don't desire any waiting effect you can simply set the entrypoint to `./goldapps`. + +For some reason `entrypoint` has to be specified in the compose file or the docker run command. + +The command should be your flags for the `goldapps` command + +See `prod.docker-compose.yaml` as reference. ### Command `goldapps` @@ -29,5 +40,9 @@ The following flags are available: * `-dry`: Makes sure the program does not change anything. * `-from someString`: Set the group source to `ldap`, `gapps` or `*.json`. In case of `gapps` config value `gapps.provider` will be used. * `-to someString`: Set the group consumer to 'gapps' or '*.json'. In case of `gapps` config value `gapps.consumer` will be used. +* `-users`: Only collect and sync users +* `-groups`: Only collect and sync groups +* `-additions *.json`: file with additions + Notice that flags should be combined on the form `goldapps -a -b` and **NOT** on the form `goldapps -ab`. \ No newline at end of file diff --git a/action.go b/action.go deleted file mode 100644 index a2cdaa1..0000000 --- a/action.go +++ /dev/null @@ -1,133 +0,0 @@ -package goldapps - -import ( - "bytes" - "fmt" -) - -type GroupUpdate struct { - Before Group - After Group -} - -type Actions struct { - Updates []GroupUpdate - Additions []Group - Deletions []Group -} - -func printProgress(done int, total int) { - p := (done * 100) / total - builder := bytes.Buffer{} - for i := 0; i < 100; i++ { - if i < p { - builder.WriteByte('=') - } else if i == p { - builder.WriteByte('>') - } else { - builder.WriteByte(' ') - } - - } - fmt.Printf("\rProgress: [%s] %d/%d", builder.String(), done, total) - if done == total { - fmt.Printf("\rDone\n") - } -} - -// Commits a set of actions to a service. -// Returns all actions performed and a error if not all actions could be performed for some reason. -func (actions Actions) Commit(service GroupUpdateService) (Actions, error) { - - performedActions := Actions{} - - if len(actions.Updates) > 0 { - fmt.Println("Performing updates") - } - for _, update := range actions.Updates { - err := service.UpdateGroup(update) - if err != nil { - fmt.Println() - return performedActions, err - } - - performedActions.Updates = append(performedActions.Updates, update) - printProgress(len(performedActions.Updates), len(actions.Updates)) - } - - if len(actions.Additions) > 0 { - fmt.Println("Performing additions") - } - for _, group := range actions.Additions { - err := service.AddGroup(group) - if err != nil { - fmt.Println() - return performedActions, err - } - - performedActions.Additions = append(performedActions.Additions, group) - printProgress(len(performedActions.Additions), len(actions.Additions)) - } - - - if len(actions.Deletions) > 0 { - fmt.Println("Performing deletions") - } - for _, group := range actions.Deletions { - err := service.DeleteGroup(group) - if err != nil { - fmt.Println() - return performedActions, err - } - - performedActions.Deletions = append(performedActions.Deletions, group) - printProgress(len(performedActions.Deletions), len(actions.Deletions)) - } - - return performedActions, nil -} - -// Determines actions required to make the "old" group list look as the "new" group list. -// Returns a list with those actions. -func ActionsRequired(old []Group, new []Group) Actions { - requiredActions := Actions{} - - for _, newGroup := range new { - - exists := false - for _, oldGroup := range old { - if newGroup.Email == oldGroup.Email { - exists = true - if !newGroup.equals(oldGroup) { // Group exists but is modified - requiredActions.Updates = append(requiredActions.Updates, GroupUpdate{ - Before: oldGroup, - After: newGroup, - }) - } - break - } - } - - if !exists { // Group does not exist in old list - requiredActions.Additions = append(requiredActions.Additions, newGroup) - } - } - - for _, oldGroup := range old { - - exists := false - for _, newGroup := range new { - if oldGroup.Email == newGroup.Email { - exists = true - break - } - } - - if !exists { // Old list has group but the new list doesn't - requiredActions.Deletions = append(requiredActions.Deletions, oldGroup) - } - - } - - return requiredActions -} diff --git a/admin/scope.go b/admin/scope.go deleted file mode 100644 index 847a4ec..0000000 --- a/admin/scope.go +++ /dev/null @@ -1,9 +0,0 @@ -package admin - -import "google.golang.org/api/admin/directory/v1" - -func Scopes() []string { - return []string{ - admin.AdminDirectoryGroupScope, - } -} diff --git a/admin/service.go b/admin/service.go deleted file mode 100644 index d1d073a..0000000 --- a/admin/service.go +++ /dev/null @@ -1,281 +0,0 @@ -package admin - -import ( - "google.golang.org/api/admin/directory/v1" // Imports as admin - - "io/ioutil" - - "bytes" - "fmt" - "github.com/cthit/goldapps" - "golang.org/x/net/context" - "golang.org/x/oauth2/google" - "time" - "strings" -) - -type googleService struct { - service *admin.Service -} - -func NewGoogleService(keyPath string, adminMail string) (goldapps.GroupUpdateService, error) { - - jsonKey, err := ioutil.ReadFile(keyPath) - if err != nil { - return nil, err - } - - // Parse jsonKey - config, err := google.JWTConfigFromJSON(jsonKey, Scopes()...) - if err != nil { - return nil, err - } - - // Why do I need this?? - config.Subject = adminMail - - // Create a http client - client := config.Client(context.Background()) - - service, err := admin.New(client) - if err != nil { - return nil, err - } - - gs := googleService{ - service: service, - } - - return gs, nil -} - -func (s googleService) DeleteGroup(group goldapps.Group) error { - return s.deleteGroup(group.Email) -} - -func (s googleService) UpdateGroup(groupUpdate goldapps.GroupUpdate) error { - newGroup := admin.Group{ - Email: groupUpdate.Before.Email, - } - - // Add all new members - for _, member := range groupUpdate.After.Members { - exists := false - for _, existingMember := range groupUpdate.Before.Members { - if strings.ToLower(member) == strings.ToLower(existingMember) { - exists = true - break - } - } - if !exists { - err := s.addMember(groupUpdate.Before.Email, member) - if err != nil { - fmt.Printf("Failed to add menber %s\n",member) - return err - } - } - } - - // Remove all old members - for _, existingMember := range groupUpdate.Before.Members { - keep := false - for _, member := range groupUpdate.After.Members { - if strings.ToLower(existingMember) == strings.ToLower(member) { - keep = true - break - } - } - if !keep { - err := s.deleteMember(groupUpdate.Before.Email, existingMember) - if err != nil { - return err - } - } - } - - // Add all new aliases - for _, alias := range groupUpdate.After.Aliases { - exists := false - for _, existingAlias := range groupUpdate.Before.Aliases { - if strings.ToLower(alias) == strings.ToLower(existingAlias) { - exists = true - break - } - } - if !exists { - err := s.addAlias(groupUpdate.Before.Email, alias) - if err != nil { - return err - } - } - } - - // Remove all old aliases - for _, existingAlias := range groupUpdate.Before.Aliases { - keep := false - for _, alias := range groupUpdate.After.Aliases { - if strings.ToLower(existingAlias) == strings.ToLower(alias) { - keep = true - break - } - } - if !keep { - err := s.deleteAlias(groupUpdate.Before.Email, existingAlias) - if err != nil { - return err - } - } - } - - return s.updateGroup(newGroup) -} - -func (s googleService) AddGroup(group goldapps.Group) error { - newGroup := admin.Group{ - Email: group.Email, - } - - err := s.addGroup(newGroup) - if err != nil { - return err - } - - time.Sleep(time.Second*10) - - // Add members - for _, member := range group.Members { - err = s.addMember(group.Email, member) - if err != nil { - return err - } - } - - // Add Aliases - for _, alias := range group.Aliases { - err = s.addAlias(group.Email, alias) - if err != nil { - return err - } - } - return nil -} - -func (s googleService) GetGroups() ([]goldapps.Group, error) { - - adminGroups, err := s.getGroups("my_customer") - if err != nil { - return nil, err - } - - groups := make([]goldapps.Group, len(adminGroups)) - for i, group := range adminGroups { - - p := (i * 100) / len(groups) - - builder := bytes.Buffer{} - for i := 0; i < 100; i++ { - if i < p { - builder.WriteByte('=') - } else if i == p { - builder.WriteByte('>') - } else { - builder.WriteByte(' ') - } - - } - - fmt.Printf("\rProgress: [%s] %d/%d", builder.String(), i+1, len(groups)) - - members, err := s.getMembers(group.Email) - if err != nil { - return nil, err - } - - groups[i] = goldapps.Group{ - Email: group.Email, - Members: members, - Aliases: group.Aliases, - } - } - fmt.Printf("\rDone\n") - - return groups, nil - -} - -func (s googleService) getGroups(customer string) ([]admin.Group, error) { - groups, err := s.service.Groups.List().Customer(customer).Do() - if err != nil { - return nil, err - } - - for groups.NextPageToken != "" { - newGroups, err := s.service.Groups.List().Customer(customer).PageToken(groups.NextPageToken).Do() - if err != nil { - return nil, err - } - - groups.Groups = append(groups.Groups, newGroups.Groups...) - groups.NextPageToken = newGroups.NextPageToken - } - - result := make([]admin.Group, len(groups.Groups)) - for i, group := range groups.Groups { - result[i] = *group - } - - return result, nil -} - -func (s googleService) getMembers(email string) ([]string, error) { - members, err := s.service.Members.List(email).Do() - if err != nil { - return nil, err - } - - result := make([]string, len(members.Members)) - for i, member := range members.Members { - result[i] = member.Email - } - - return result, nil -} - -func (s googleService) getGroup(email string) (admin.Group, error) { - group, err := s.service.Groups.Get(email).Do() - - return *group, err -} - -func (s googleService) addGroup(group admin.Group) error { - _, err := s.service.Groups.Insert(&group).Do() - return err -} - -func (s googleService) updateGroup(group admin.Group) error { - _, err := s.service.Groups.Update(group.Email, &group).Do() - return err -} - -func (s googleService) deleteGroup(email string) error { - err := s.service.Groups.Delete(email).Do() - return err -} - -func (s googleService) deleteMember(groupEmail string, member string) error { - return s.service.Members.Delete(groupEmail, member).Do() -} - -func (s googleService) addMember(groupEmail string, memberEmail string) error { - _, err := s.service.Members.Insert(groupEmail, &admin.Member{Email: memberEmail}).Do() - return err -} - -func (s googleService) deleteAlias(groupEmail string, alias string) error { - return s.service.Groups.Aliases.Delete(groupEmail, alias).Do() -} - -func (s googleService) addAlias(groupEmail string, alias string) error { - _, err := s.service.Groups.Aliases.Insert(groupEmail, &admin.Alias{Alias: alias}).Do() - return err -} diff --git a/cmd/chalmers.it.config.toml b/cmd/chalmers.it.config.toml deleted file mode 100644 index 05b7e2f..0000000 --- a/cmd/chalmers.it.config.toml +++ /dev/null @@ -1,64 +0,0 @@ -[gapps.consumer] - servicekeyfile = "gapps.json" - adminaccount = "admin@chalmers.it" - -[gapps.provider] - servicekeyfile = "gapps.json" - adminaccount = "admin@chalmers.it" - -[ldap] - url = "ldap.chalmers.it:636" - servername = "chalmers.it" - user = "cn=SERVICE_ACCOUNT_GOES_HERE,dc=chalmers,dc=it" - password = "PASSWORD_GOES_HERE" - custom = ["chairman", "chairmen.fkit", "chairmen.committees", "treasurers", "phadderchef", "fkit"] - -[ldap.groups] - basedn = "ou=groups,dc=chalmers,dc=it" - filter = "(|(objectClass=itGroup)(objectClass=itPosition))" - attibutes = ["cn", "displayName", "mail", "member"] - -[ldap.users] - basedn = "ou=people,dc=chalmers,dc=it" - filter = "(&(objectClass=chalmersstudent))" - attibutes = ["uid", "mail"] - -#### CUSTOM FILTERS #### -[ldap.fkit] - mail = "fkit@chalmers.it" - basedn = "ou=fkit,ou=groups,dc=chalmers,dc=it" - filter = "(&(objectClass=itGroup))" - parent_filter = "(&(ou=%childRDN%))" - attibutes = ["cn", "displayName", "mail"] - -[ldap.chairman] - mail = "ordforande@chalmers.it" - basedn = "ou=styrit,ou=fkit,ou=groups,dc=chalmers,dc=it" - filter = "(&(objectClass=itPosition)(cn=ordf))" - attibutes = ["cn", "displayName", "mail"] - -[ldap.chairmen.fkit] - mail = "ordforanden@chalmers.it" - basedn = "ou=fkit,ou=groups,dc=chalmers,dc=it" - filter = "(&(objectClass=itPosition)(cn=ordf))" - attibutes = ["cn", "displayName", "mail"] - -[ldap.chairmen.committees] - mail = "ordforanden.kommiteer@chalmers.it" - basedn = "ou=fkit,ou=groups,dc=chalmers,dc=it" - filter = "(&(objectClass=itPosition)(cn=ordf))" - parent_filter = "(&(objectClass=itGroup)(type=Committee))" - attibutes = ["cn", "displayName", "mail", "type"] - -[ldap.treasurers] - mail = "kassorer@chalmers.it" - basedn = "ou=fkit,ou=groups,dc=chalmers,dc=it" - filter = "(&(objectClass=itPosition)(cn=kassor))" - attibutes = ["cn", "displayName", "mail"] - -[ldap.phadderchef] - mail = "phadderchef@chalmers.it" - basedn = "ou=nollkit,ou=fkit,ou=groups,dc=chalmers,dc=it" - filter = "(&(objectClass=itPosition)(cn=phadderchef))" - attibutes = ["cn", "displayName", "mail"] -#### ============== #### diff --git a/cmd/example.config.toml b/cmd/example.config.toml deleted file mode 100644 index 956dad0..0000000 --- a/cmd/example.config.toml +++ /dev/null @@ -1,31 +0,0 @@ -[gapps.consumer] - servicekeyfile = "gapps1.json" - adminaccount = "admin@example1.ex" - -[gapps.provider] - servicekeyfile = "gapps2.json" - adminaccount = "admin@example2.ex" - -[ldap] - url = "ldap.example.ex:999" - servername = "example.ex" - user = "cn=god,dc=example,dc=ex" - password = "secret" - custom = ["my_custom_filter"] - -[ldap.groups] - basedn = "ou=some,ou=groups,dc=example,dc=ex" - filter = "(&(objectClass=Group))" - attibutes = ["cn", "displayName", "mail", "member"] - -[ldap.users] - basedn = "ou=people,dc=example,dc=ex" - filter = "(&(objectClass=Group))" - attibutes = ["uid", "mail"] - -[ldap.my_custom_filter] - mail = "custom@example.ex" - basedn = "ou=groups,dc=chalmers,dc=it" - filter = "(&(objectClass=Group))" - parent_filter = "(&(ou=%childRDN%))" - attibutes = ["cn", "displayName", "mail"] diff --git a/cmd/flags.go b/cmd/flags.go deleted file mode 100644 index 043872e..0000000 --- a/cmd/flags.go +++ /dev/null @@ -1,22 +0,0 @@ -package main - -import "flag" - -type flagStruct struct { - from string - to string - interactive bool - noInteraction bool - dryRun bool -} - -var flags = flagStruct{} - -func loadFlags() { - flag.StringVar(&flags.from, "from", "ldap", "Set the group source to 'ldap', 'gapps' or '*.json'. In case of gapps config value 'gappsProvider' will be used") - flag.StringVar(&flags.to, "to", "gapps", "Set the group consumer to 'gapps' or '*.json'") - flag.BoolVar(&flags.dryRun, "dry", false, "Setting this flag will cause the application to only print information and not update any groups") - flag.BoolVar(&flags.noInteraction, "y", false, "Setting this flag will cause the application to not ask for any user confirmation") - flag.BoolVar(&flags.interactive, "i", false, "Setting this flag will cause the application to ask the user for input in every stage ") - flag.Parse() -} diff --git a/cmd/goldapps/main.go b/cmd/goldapps/main.go new file mode 100644 index 0000000..b0fee8e --- /dev/null +++ b/cmd/goldapps/main.go @@ -0,0 +1,7 @@ +package main + +import "github.com/cthit/goldapps/internal/app/cli" + +func main() { + cli.Run() +} diff --git a/cmd/ldap.go b/cmd/ldap.go deleted file mode 100644 index b4e3395..0000000 --- a/cmd/ldap.go +++ /dev/null @@ -1,47 +0,0 @@ -package main - -import ( - "github.com/cthit/goldapps/ldap" - "github.com/spf13/viper" -) - -func NewLdapService() (*ldap.ServiceLDAP, error) { - dbConfig := ldap.ServerConfig{ - Url: viper.GetString("ldap.url"), - ServerName: viper.GetString("ldap.servername"), - } - - groupsConfig := ldap.EntryConfig{ - BaseDN: viper.GetString("ldap.groups.basedn"), - Filter: viper.GetString("ldap.groups.filter"), - Attributes: viper.GetStringSlice("ldap.groups.attributes"), - } - - usersConfig := ldap.EntryConfig{ - BaseDN: viper.GetString("ldap.users.basedn"), - Filter: viper.GetString("ldap.users.filter"), - Attributes: viper.GetStringSlice("ldap.users.attributes"), - } - - // Add custom entries - customEntryNames := viper.GetStringSlice("ldap.custom") - customEntryConfigs := make([]ldap.CustomEntryConfig, 0) - for _, entry := range customEntryNames { - customEntryConfigs = append(customEntryConfigs, - ldap.CustomEntryConfig{ - BaseDN: viper.GetString("ldap." + entry + ".basedn"), - Filter: viper.GetString("ldap." + entry + ".filter"), - ParentFilter: viper.GetString("ldap." + entry + ".parent_filter"), - Attributes: viper.GetStringSlice("ldap." + entry + ".attributes"), - Mail: viper.GetString("ldap." + entry + ".mail"), - }, - ) - } - - loginConfig := ldap.LoginConfig{ - UserName: viper.GetString("ldap.user"), - Password: viper.GetString("ldap.password"), - } - - return ldap.NewLDAPService(dbConfig, loginConfig, usersConfig, groupsConfig, customEntryConfigs) -} diff --git a/cmd/main.go b/cmd/main.go deleted file mode 100644 index 52fee60..0000000 --- a/cmd/main.go +++ /dev/null @@ -1,217 +0,0 @@ -package main - -import ( - "fmt" - "github.com/cthit/goldapps" - "github.com/cthit/goldapps/admin" - "github.com/cthit/goldapps/json" - "github.com/spf13/viper" - "regexp" -) - -func init() { - loadFlags() - - err := loadConfig() - if err != nil { - fmt.Println("Failed to load config.") - panic(err) - } - fmt.Println("Loaded config.") -} - -func main() { - - fmt.Println("Setting up provider") - provider := getProvider() - fmt.Println("Setting up consumer") - consumer := getConsumer() - - fmt.Println("Collecting groups from the provider...") - providerGroups, err := provider.GetGroups() - if err != nil { - fmt.Println("Failed to collect groups from provider") - panic(err) - } - fmt.Printf("%d groups collected.\n", len(providerGroups)) - - fmt.Println("Collecting groups from the consumer...") - consumerGroups, err := consumer.GetGroups() - if err != nil { - fmt.Println("Failed to collect groups from consumer") - panic(err) - } - fmt.Printf("%d groups collected.\n", len(consumerGroups)) - - fmt.Println("Colculating difference between the consumer and provider.") - proposedChanges := goldapps.ActionsRequired(consumerGroups, providerGroups) - changes := getChanges(proposedChanges) - - if flags.interactive { - proceed := askBool( - fmt.Sprintf( - "Are you sure you want to commit these additions(%d), deletions(%d) and updates(%d)?", - len(changes.Additions), - len(changes.Deletions), - len(changes.Updates), - ), - true, - ) - if !proceed { - fmt.Println("Done! (No changes made) Stopping application...") - return - } - } - if flags.dryRun { - fmt.Println("Done! (No changes made, dryrun) Stopping application...") - return - } - - performed, err := changes.Commit(consumer) - if err == nil { - fmt.Println("All actions performed!") - return - } else { - fmt.Println("All actions could not be performed...") - fmt.Printf("\t Performed %d out of %d Additions\n", len(performed.Additions), len(changes.Additions)) - fmt.Printf("\t Performed %d out of %d Deletions\n", len(performed.Deletions), len(changes.Deletions)) - fmt.Printf("\t Performed %d out of %d Updates\n", len(performed.Updates), len(changes.Updates)) - fmt.Printf("Error: %s", err.Error()) - } -} - -func getChanges(proposedChanges goldapps.Actions) goldapps.Actions { - if !flags.interactive && flags.noInteraction { - fmt.Printf( - "Automaticly accepting %d addition, %d deletions and %d updates\n", - len(proposedChanges.Additions), - len(proposedChanges.Deletions), - len(proposedChanges.Updates), - ) - } else { - // Handle additions - fmt.Printf("Additions (%d):\n", len(proposedChanges.Additions)) - if len(proposedChanges.Additions) > 0 { - for _, group := range proposedChanges.Additions { - fmt.Printf("\t%v\n", group) - } - add := askBool( - fmt.Sprintf("Do you want to commit those %d additions?", len(proposedChanges.Additions)), - true, - ) - if !add { - proposedChanges.Additions = nil - } - } - - // Handle Deletions - fmt.Printf("Deletions (%d):\n", len(proposedChanges.Deletions)) - if len(proposedChanges.Deletions) > 0 { - for _, group := range proposedChanges.Deletions { - fmt.Printf("\t%v\n", group) - } - add := askBool( - fmt.Sprintf("Do you want to commit those %d deletions?", len(proposedChanges.Deletions)), - true, - ) - if !add { - proposedChanges.Deletions = nil - } - } - - // Handle changes - fmt.Printf("Changes (%d):\n", len(proposedChanges.Updates)) - if len(proposedChanges.Updates) > 0 { - for _, update := range proposedChanges.Updates { - fmt.Printf("\tUpdate:\n") - fmt.Printf("\t\tFrom:\n") - fmt.Printf("\t\t\t%v\n", update.Before) - fmt.Printf("\t\tTo:\n") - fmt.Printf("\t\t\t%v\n", update.After) - } - add := askBool( - fmt.Sprintf("Do you want to commit those %d updates?", len(proposedChanges.Updates)), - true, - ) - if !add { - proposedChanges.Updates = nil - } - } - } - return proposedChanges -} - -func getConsumer() goldapps.GroupUpdateService { - var to string - if flags.interactive { - to = askString("Which consumer would you like to use, 'gapps' or '*.json?", "gapps") - } else { - to = flags.to - } - - switch to { - case "gapps": - consumer, err := admin.NewGoogleService( - viper.GetString("gapps.consumer.servicekeyfile"), - viper.GetString("gapps.consumer.adminaccount")) - if err != nil { - fmt.Println("Failed to create gapps connection.") - panic(err) - } - return consumer - default: - isJson, _ := regexp.MatchString(`.+\.json$`, to) - if isJson { - consumer, _ := json.NewJsonService(to) - return consumer - } else { - fmt.Println("You must specify 'gapps' or '*.json' as consumer.") - previous := flags.interactive - flags.interactive = true - defer func() { - flags.interactive = previous - }() - return getConsumer() - } - } -} - -func getProvider() goldapps.GroupService { - var from string - if flags.interactive { - from = askString("which provider would you like to use, 'ldap', 'gapps' or '*.json'?", "ldap") - } else { - from = flags.from - } - - switch from { - case "ldap": - provider, err := NewLdapService() - if err != nil { - fmt.Println("Failed to create LDAP connection.") - panic(err) - } - return provider - case "gapps": - provider, err := admin.NewGoogleService(viper.GetString("gapps.provider.servicekeyfile"), viper.GetString("gapps.provider.adminaccount")) - if err != nil { - fmt.Println("Failed to create gapps connection, make sure you have setup gappsProvider in the config file.") - panic(err) - } - return provider - default: - isJson, _ := regexp.MatchString(`.+\.json$`, from) - if isJson { - provider, _ := json.NewJsonService(from) - return provider - } else { - fmt.Println("You must specify 'gapps', 'ldap' or '*.json' as provider.") - previous := flags.interactive - flags.interactive = true - defer func() { - flags.interactive = previous - }() - return getProvider() - } - } -} diff --git a/example.config.toml b/example.config.toml new file mode 100644 index 0000000..8212381 --- /dev/null +++ b/example.config.toml @@ -0,0 +1,41 @@ +[gapps.consumer] + servicekeyfile = "gapps.json" + adminaccount = "admin@mydomain.ex" + +[gapps.provider] + servicekeyfile = "gapps.json" + adminaccount = "admin@mydomain.ex" + +[ldap] + url = "ldap.mydomain.ex:636" + servername = "mydomain.ex" + user = "cn=admin,dc=mydomain,dc=ex" + password = "PASSWORD" + custom = ["fkit", "kit"] + +[ldap.groups] + basedn = "ou=groups,dc=mydomain,dc=ex" + filter = "(|(objectClass=itGroup)(objectClass=itPosition))" + attibutes = ["cn", "displayName", "mail", "member"] + +[ldap.users] + basedn = "ou=people,dc=mydomain,dc=ex" + filter = "(&(objectClass=chalmersstudent))" + attibutes = ["uid", "mail"] + +#### CUSTOM FILTERS #### +[ldap.fkit] + mail = "fkit@mydomain.ex" + basedn = "ou=fkit,ou=groups,dc=mydomain,dc=ex" + filter = "(&(objectClass=itGroup))" + parent_filter = "(&(ou=%childRDN%))" + attibutes = ["cn", "displayName", "mail"] + + +[ldap.kit] + mail = "kit@mydomain.ex" + basedn = "ou=fkit,ou=groups,dc=mydomain,dc=ex" + filter = "(&(objectClass=itGroup)(type=Committee))" + parent_filter = "(&(ou=%childRDN%))" + attibutes = ["cn", "displayName", "mail"] +#### ============== #### diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2466d6f --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module github.com/cthit/goldapps + +go 1.12 + +require ( + github.com/magiconair/properties v1.8.0 + github.com/sethvargo/go-password v0.1.2 + github.com/spf13/viper v1.3.2 + golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5 + golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a + google.golang.org/api v0.5.0 + gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect + gopkg.in/ldap.v2 v2.5.1 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..ae982d9 --- /dev/null +++ b/go.sum @@ -0,0 +1,92 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sethvargo/go-password v0.1.2 h1:fhBF4thiPVKEZ7R6+CX46GWJiPyCyXshbeqZ7lqEeYo= +github.com/sethvargo/go-password v0.1.2/go.mod h1:qKHfdSjT26DpHQWHWWR5+X4BI45jT31dg6j4RI2TEb0= +github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5 h1:6M3SDHlHHDCx2PcQw3S4KsR170vGqDhJDOmpVd4Hjak= +golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a h1:tImsplftrFpALCYumobsd0K86vlAs/eXGFms2txfJfA= +golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +google.golang.org/api v0.5.0 h1:lj9SyhMzyoa38fgFF0oO2T6pjs5IzkLPKfVtxpyCRMM= +google.golang.org/api v0.5.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19 h1:Lj2SnHtxkRGJDqnGaSjo+CCdIieEnwVazbOXILwQemk= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/grpc v1.19.0 h1:cfg4PD8YEdSFnm7qLV4++93WcmhH2nIUhMjhdCvl3j8= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d h1:TxyelI5cVkbREznMhfzycHdkp5cLA7DpE+GKjSslYhM= +gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ldap.v2 v2.5.1 h1:wiu0okdNfjlBzg6UWvd1Hn8Y+Ux17/u/4nlk4CQr6tU= +gopkg.in/ldap.v2 v2.5.1/go.mod h1:oI0cpe/D7HRtBQl8aTg+ZmzFUAvu4lsv3eLXMLGFxWk= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/group.go b/group.go deleted file mode 100644 index b0c6377..0000000 --- a/group.go +++ /dev/null @@ -1,53 +0,0 @@ -package goldapps - -import "strings" - -// Represents a email group. -// Email is the id and main email for the group. -// Members is a lost of email addresses that are members of this group. -// Aliases are alternative email addresses for the group. -type Group struct { - Email string `json:"email"` - Members []string `json:"members"` - Aliases []string `json:"aliases"` -} - -func (group Group) equals(other Group) bool { - if strings.ToLower(group.Email) != strings.ToLower(other.Email) { - return false - } - if len(group.Members) != len(other.Members) { - return false - } - if len(group.Aliases) != len(other.Aliases) { - return false - } - - for _, member := range group.Members { - contains := false - for _, otherMember := range other.Members { - if strings.ToLower(member) == strings.ToLower(otherMember) { - contains = true - break - } - } - if !contains { - return false - } - } - - for _, alias := range group.Aliases { - contains := false - for _, otheralias := range other.Aliases { - if strings.ToLower(alias) == strings.ToLower(otheralias) { - contains = true - break - } - } - if !contains { - return false - } - } - - return true -} diff --git a/internal/app/cli/additions.go b/internal/app/cli/additions.go new file mode 100644 index 0000000..4ba6c60 --- /dev/null +++ b/internal/app/cli/additions.go @@ -0,0 +1,135 @@ +package cli + +import ( + "fmt" + "github.com/cthit/goldapps/internal/pkg/model" + "github.com/cthit/goldapps/internal/pkg/services/json" + "regexp" +) + +func addAdditions(providerGroups model.Groups, providerUsers model.Users) (model.Groups, model.Users) { + fmt.Println("Collecting additions") + additionUsers, additionGroups := getAdditions() + if additionUsers != nil && additionGroups != nil { + fmt.Printf("%d usersAdditions and %d groupAdditions collected.\n", len(additionUsers), len(additionGroups)) + + fmt.Print("Merging groups... ") + providerGroups = mergeAdditionGroups(additionGroups, providerGroups) + fmt.Println("Done!") + + fmt.Print("Merging users... ") + providerUsers = mergeAdditionalUsers(additionUsers, providerUsers) + fmt.Println("Done!") + } else { + fmt.Println("Skipping additions") + } + return providerGroups, providerUsers +} + +func getAdditions() ([]model.User, []model.Group) { + + var from string + if flags.interactive { + from = askString("Which file would you like to use for additions?, Just press enter to skip", "") + } else { + from = flags.additions + } + + if from == "" { + return nil, nil + } + + isJson, _ := regexp.MatchString(`.+\.json$`, from) + if isJson { + provider, _ := json.NewJsonService(from) + groups, err := provider.GetGroups() + if err != nil { + panic(err) + } + users, err := provider.GetUsers() + if err != nil { + panic(err) + } + return users, groups + } else { + fmt.Println("You must specify a valid json file") + previous := flags.interactive + flags.interactive = true + defer func() { + flags.interactive = previous + }() + return getAdditions() + } +} + +func mergeAdditionGroups(additionGroups model.Groups, providerGroups model.Groups) model.Groups { + for _, group := range additionGroups { + found := false + for i, pgroup := range providerGroups { + if pgroup.Email == group.Email { + found = true + + // Add properties if found + for _, alias := range group.Aliases { + aliasFound := false + for _, other := range pgroup.Aliases { + if other == alias { + aliasFound = true + } + } + if !aliasFound { + providerGroups[i].Aliases = append(pgroup.Aliases, alias) + } + } + + for _, member := range group.Members { + memberFound := false + for _, other := range pgroup.Members { + if other == member { + memberFound = true + } + } + if !memberFound { + providerGroups[i].Members = append(pgroup.Members, member) + } + } + } + } + + // Otherwise simply append it + if !found { + providerGroups = append(providerGroups, group) + } + } + return providerGroups +} +func mergeAdditionalUsers(additionUsers model.Users, providerUsers model.Users) model.Users { + for _, user := range additionUsers { + found := false + for i, pUser := range providerUsers { + + // Add Properties if found, never replace tho + if pUser.Cid == user.Cid { + found = true + if user.Nick != "" { + providerUsers[i].Nick = user.Nick + } + if user.SecondName != "" { + providerUsers[i].SecondName = user.SecondName + } + if user.FirstName != "" { + providerUsers[i].FirstName = user.FirstName + } + if user.Mail != "" { + providerUsers[i].Mail = user.Mail + } + } + } + + // Just add user if it wasn't found + if !found { + providerUsers = append(providerUsers, user) + } + } + return providerUsers +} diff --git a/internal/app/cli/bgc.go b/internal/app/cli/bgc.go new file mode 100644 index 0000000..b06d937 --- /dev/null +++ b/internal/app/cli/bgc.go @@ -0,0 +1,233 @@ +package cli + +import ( + "fmt" + "github.com/cthit/goldapps/internal/pkg/actions" + "github.com/cthit/goldapps/internal/pkg/duplicates" + "github.com/cthit/goldapps/internal/pkg/model" +) + +func init() { + loadFlags() + + err := loadConfig() + if err != nil { + fmt.Println("Failed to load config.") + panic(err) + } + fmt.Println("Loaded config.") +} + +func Run() { + + fmt.Println("Setting up providers") + provider := getProvider() + + fmt.Println("Setting up services") + consumer := getConsumer() + + // Collect users and groups + var providerUsers model.Users + var consumerUsers model.Users + var providerGroups model.Groups + var consumerGroups model.Groups + if !flags.onlyUsers { + fmt.Println("Collecting groups from the providers...") + providerGroups = collectGroups(provider) + fmt.Println("Collecting groups from the services...") + consumerGroups = collectGroups(consumer) + } + if !flags.onlyGroups { + fmt.Println("Collecting users from the providers...") + providerUsers = collectUsers(provider) + fmt.Println("Collecting users from the services...") + consumerUsers = collectUsers(consumer) + } + + // Get and process additions + providerGroups, providerUsers = addAdditions(providerGroups, providerUsers) + + // Check for and handle duplicates + providerUsers, providerGroups = duplicates.RemoveDuplicates(providerUsers, providerGroups) + + // Get changes to make + groupChanges := actions.GroupActions{} + if !flags.onlyUsers { + fmt.Println("Colculating difference between the services and providers groups.") + proposedGroupChanges := actions.GroupActionsRequired(consumerGroups, providerGroups) + groupChanges = getGroupChanges(proposedGroupChanges) + } + userChanges := actions.UserActions{} + if !flags.onlyGroups { + fmt.Println("Colculating difference between the services and providers users.") + proposedUserChanges := actions.UserActionsRequired(consumerUsers, providerUsers) + userChanges = getUserChanges(proposedUserChanges) + } + + // Ask for confirmation if we are in interactive mode + if flags.interactive { + proceed := askBool( + fmt.Sprintf( + "Are you sure you want to commit these (groups + users) additions(%d + %d), deletions(%d + %d) and updates(%d + %d)?", + len(groupChanges.Additions), + len(userChanges.Additions), + len(groupChanges.Deletions), + len(userChanges.Deletions), + len(groupChanges.Updates), + len(userChanges.Updates), + ), + true, + ) + if !proceed { + fmt.Println("Done! (No changes made) Stopping application...") + return + } + } + + // Stop application if dryrun + if flags.dryRun { + fmt.Println("Done! (No changes made, dryrun) Stopping application...") + return + } + + // Commit changes + userErrors := userChanges.Commit(consumer) + groupErrors := groupChanges.Commit(consumer) + + // Print result + if groupErrors.Amount() == 0 { + fmt.Println("All groups actions performed!") + } else { + fmt.Printf("%d out of %d group actions performed\n", groupChanges.Amount()-groupErrors.Amount(), groupChanges.Amount()) + fmt.Print(groupErrors.String()) + } + if userErrors.Amount() == 0 { + fmt.Println("All users actions performed!") + } else { + fmt.Printf("%d out of %d group actions performed\n", userChanges.Amount()-userErrors.Amount(), groupChanges.Amount()) + fmt.Print(userErrors.String()) + } +} + +func getGroupChanges(proposedChanges actions.GroupActions) actions.GroupActions { + if !flags.interactive && flags.noInteraction { + fmt.Printf( + "(Groups) Automaticly accepting %d addition, %d deletions and %d updates\n", + len(proposedChanges.Additions), + len(proposedChanges.Deletions), + len(proposedChanges.Updates), + ) + } else { + // Handle Deletions + fmt.Printf("(Groups) Deletions (%d):\n", len(proposedChanges.Deletions)) + if len(proposedChanges.Deletions) > 0 { + for _, group := range proposedChanges.Deletions { + fmt.Printf("\t%v\n", group) + } + add := askBool( + fmt.Sprintf("(Groups) Do you want to commit those %d deletions?", len(proposedChanges.Deletions)), + true, + ) + if !add { + proposedChanges.Deletions = nil + } + } + + // Handle changes + fmt.Printf("(Groups) Changes (%d):\n", len(proposedChanges.Updates)) + if len(proposedChanges.Updates) > 0 { + for _, update := range proposedChanges.Updates { + fmt.Printf("\tUpdate:\n") + fmt.Printf("\t\tFrom:\n") + fmt.Printf("\t\t\t%v\n", update.Before) + fmt.Printf("\t\tTo:\n") + fmt.Printf("\t\t\t%v\n", update.After) + } + add := askBool( + fmt.Sprintf("(Groups) Do you want to commit those %d updates?", len(proposedChanges.Updates)), + true, + ) + if !add { + proposedChanges.Updates = nil + } + } + + // Handle additions + fmt.Printf("(Groups) Additions (%d):\n", len(proposedChanges.Additions)) + if len(proposedChanges.Additions) > 0 { + for _, group := range proposedChanges.Additions { + fmt.Printf("\t%v\n", group) + } + add := askBool( + fmt.Sprintf("(Groups) Do you want to commit those %d additions?", len(proposedChanges.Additions)), + true, + ) + if !add { + proposedChanges.Additions = nil + } + } + } + return proposedChanges +} + +func getUserChanges(proposedChanges actions.UserActions) actions.UserActions { + if !flags.interactive && flags.noInteraction { + fmt.Printf( + "(Users) Automaticly accepting %d addition, %d deletions and %d updates\n", + len(proposedChanges.Additions), + len(proposedChanges.Deletions), + len(proposedChanges.Updates), + ) + } else { + // Handle Deletions + fmt.Printf("(Users) Deletions (%d):\n", len(proposedChanges.Deletions)) + if len(proposedChanges.Deletions) > 0 { + for _, user := range proposedChanges.Deletions { + fmt.Printf("\t%v\n", user) + } + add := askBool( + fmt.Sprintf("(Users) Do you want to commit those %d deletions?", len(proposedChanges.Deletions)), + true, + ) + if !add { + proposedChanges.Deletions = nil + } + } + + // Handle changes + fmt.Printf("(Users) Changes (%d):\n", len(proposedChanges.Updates)) + if len(proposedChanges.Updates) > 0 { + for _, update := range proposedChanges.Updates { + fmt.Printf("\tUpdate:\n") + fmt.Printf("\t\tFrom:\n") + fmt.Printf("\t\t\t%v\n", update.Before) + fmt.Printf("\t\tTo:\n") + fmt.Printf("\t\t\t%v\n", update.After) + } + add := askBool( + fmt.Sprintf("(Users) Do you want to commit those %d updates?", len(proposedChanges.Updates)), + true, + ) + if !add { + proposedChanges.Updates = nil + } + } + + // Handle additions + fmt.Printf("(Users) Additions (%d):\n", len(proposedChanges.Additions)) + if len(proposedChanges.Additions) > 0 { + for _, user := range proposedChanges.Additions { + fmt.Printf("\t%v\n", user) + } + add := askBool( + fmt.Sprintf("(Users) Do you want to commit those %d additions?", len(proposedChanges.Additions)), + true, + ) + if !add { + proposedChanges.Additions = nil + } + } + + } + return proposedChanges +} diff --git a/cmd/config.go b/internal/app/cli/config.go similarity index 97% rename from cmd/config.go rename to internal/app/cli/config.go index f3bc89d..9085474 100644 --- a/cmd/config.go +++ b/internal/app/cli/config.go @@ -1,4 +1,4 @@ -package main +package cli import ( "github.com/spf13/viper" diff --git a/internal/app/cli/flags.go b/internal/app/cli/flags.go new file mode 100644 index 0000000..ce7dbd6 --- /dev/null +++ b/internal/app/cli/flags.go @@ -0,0 +1,28 @@ +package cli + +import "flag" + +type flagStruct struct { + from string + to string + additions string + interactive bool + noInteraction bool + dryRun bool + onlyGroups bool + onlyUsers bool +} + +var flags = flagStruct{} + +func loadFlags() { + flag.StringVar(&flags.from, "from", "ldap", "Set the source to 'ldap', 'gapps' or '*.json'. In case of gapps config value 'gappsProvider' will be used") + flag.StringVar(&flags.additions, "additions", "", "Set a json file for additional groups and users") + flag.StringVar(&flags.to, "to", "gapps", "Set the services to 'gapps' or '*.json'") + flag.BoolVar(&flags.dryRun, "dry", false, "Setting this flag will cause the application to only print information and not update any groups") + flag.BoolVar(&flags.noInteraction, "y", false, "Setting this flag will cause the application to not ask for any user confirmation") + flag.BoolVar(&flags.interactive, "i", false, "Setting this flag will cause the application to ask the user for input in every stage ") + flag.BoolVar(&flags.onlyGroups, "groups", false, "Setting this flag will cause the application to only collect and update groups") + flag.BoolVar(&flags.onlyUsers, "users", false, "Setting this flag will cause the application to only collect and update users ") + flag.Parse() +} diff --git a/internal/app/cli/services.go b/internal/app/cli/services.go new file mode 100644 index 0000000..5f5a14f --- /dev/null +++ b/internal/app/cli/services.go @@ -0,0 +1,148 @@ +package cli + +import ( + "fmt" + "github.com/cthit/goldapps/internal/pkg/model" + "github.com/cthit/goldapps/internal/pkg/services" + "github.com/cthit/goldapps/internal/pkg/services/admin" + "github.com/cthit/goldapps/internal/pkg/services/json" + "github.com/cthit/goldapps/internal/pkg/services/ldap" + "github.com/spf13/viper" + "regexp" +) + +func getConsumer() services.UpdateService { + var to string + if flags.interactive { + to = askString("Which services would you like to use, 'gapps' or '*.json?", "gapps") + } else { + to = flags.to + } + + switch to { + case "gapps": + consumer, err := admin.NewGoogleService( + viper.GetString("gapps.consumer.servicekeyfile"), + viper.GetString("gapps.consumer.adminaccount")) + if err != nil { + fmt.Println("Failed to create gapps connection.") + panic(err) + } + return consumer + default: + isJson, _ := regexp.MatchString(`.+\.json$`, to) + if isJson { + consumer, _ := json.NewJsonService(to) + return consumer + } else { + fmt.Println("You must specify 'gapps' or '*.json' as services.") + previous := flags.interactive + flags.interactive = true + defer func() { + flags.interactive = previous + }() + return getConsumer() + } + } +} + +func getProvider() services.CollectionService { + var from string + if flags.interactive { + from = askString("which providers would you like to use, 'ldap', 'gapps' or '*.json'?", "ldap") + } else { + from = flags.from + } + + switch from { + case "ldap": + provider, err := newLdapService() + if err != nil { + fmt.Println("Failed to create LDAP connection.") + panic(err) + } + return provider + case "gapps": + provider, err := admin.NewGoogleService(viper.GetString("gapps.provider.servicekeyfile"), viper.GetString("gapps.provider.adminaccount")) + if err != nil { + fmt.Println("Failed to create gapps connection, make sure you have setup gappsProvider in the config file.") + panic(err) + } + return provider + default: + isJson, _ := regexp.MatchString(`.+\.json$`, from) + if isJson { + provider, _ := json.NewJsonService(from) + return provider + } else { + fmt.Println("You must specify 'gapps', 'ldap' or '*.json' as providers.") + previous := flags.interactive + flags.interactive = true + defer func() { + flags.interactive = previous + }() + return getProvider() + } + } +} + +func collectGroups(service services.CollectionService) model.Groups { + groups, err := service.GetGroups() + if err != nil { + fmt.Println("Failed to collect groups") + panic(err) + } + fmt.Printf("%d groups collected.\n", len(groups)) + return groups +} + +func collectUsers(service services.CollectionService) model.Users { + users, err := service.GetUsers() + if err != nil { + fmt.Println("Failed to collect users") + panic(err) + } + fmt.Printf("%d users collected.\n", len(users)) + return users +} + +func newLdapService() (*ldap.ServiceLDAP, error) { + dbConfig := ldap.ServerConfig{ + Url: viper.GetString("ldap.url"), + ServerName: viper.GetString("ldap.servername"), + } + + groupsConfig := ldap.EntryConfig{ + BaseDN: viper.GetString("ldap.groups.basedn"), + Filter: viper.GetString("ldap.groups.filter"), + Attributes: viper.GetStringSlice("ldap.groups.attributes"), + } + + usersConfig := ldap.EntryConfig{ + BaseDN: viper.GetString("ldap.users.basedn"), + Filter: viper.GetString("ldap.users.filter"), + Attributes: viper.GetStringSlice("ldap.users.attributes"), + } + + // Add custom entries + customEntryNames := viper.GetStringSlice("ldap.custom") + customEntryConfigs := make([]ldap.CustomEntryConfig, 0) + for _, entry := range customEntryNames { + customEntryConfigs = append(customEntryConfigs, + ldap.CustomEntryConfig{ + BaseDN: viper.GetString("ldap." + entry + ".basedn"), + Filter: viper.GetString("ldap." + entry + ".filter"), + ParentFilter: viper.GetString("ldap." + entry + ".parent_filter"), + Attributes: viper.GetStringSlice("ldap." + entry + ".attributes"), + Mail: viper.GetString("ldap." + entry + ".mail"), + }, + ) + } + + loginConfig := ldap.LoginConfig{ + UserName: viper.GetString("ldap.user"), + Password: viper.GetString("ldap.password"), + } + + return ldap.NewLDAPService(dbConfig, loginConfig, usersConfig, groupsConfig, customEntryConfigs) +} diff --git a/cmd/util.go b/internal/app/cli/util.go similarity index 50% rename from cmd/util.go rename to internal/app/cli/util.go index 049741c..a8c5eb2 100644 --- a/cmd/util.go +++ b/internal/app/cli/util.go @@ -1,6 +1,9 @@ -package main +package cli -import "fmt" +import ( + "bytes" + "fmt" +) func askBool(question string, preferred bool) bool { if preferred { @@ -36,3 +39,32 @@ func askString(question string, preferred string) string { return input } } + +// Done does include failed too +func printProgress(done, total, failed int) { + p := (done * 100) / total + builder := bytes.Buffer{} + for i := 0; i <= 100; i++ { + if i < p { + builder.WriteByte('=') + } else if i == p { + builder.WriteByte('>') + } else { + builder.WriteByte(' ') + } + } + fmt.Printf("\rProgress: [%s] %d/%d", builder.String(), done, total) + + // Add failed counter if necessary + if failed != 0 { + fmt.Printf(" (Failed: %d)", failed) + } + + // Replace progressbar with done text + if done == total { + if failed != 0 { + fmt.Printf("Done! (Failed: %d)", failed) + } + fmt.Printf("\rDone\n") + } +} diff --git a/internal/pkg/actions/group_action.go b/internal/pkg/actions/group_action.go new file mode 100644 index 0000000..1f4e48f --- /dev/null +++ b/internal/pkg/actions/group_action.go @@ -0,0 +1,145 @@ +package actions + +import ( + "bytes" + "fmt" + "github.com/cthit/goldapps/internal/pkg/model" + "github.com/cthit/goldapps/internal/pkg/services" +) + +// Set of action, to be performed on a set of groups +type GroupActions struct { + Updates []model.GroupUpdate + Additions []model.Group + Deletions []model.Group +} + +func (actions GroupActions) Amount() int { + return len(actions.Additions) + len(actions.Deletions) + len(actions.Updates) +} + +// Set of actions that could not be performed with accompanying errors +type GroupActionErrors struct { + Updates []GroupUpdateError + Additions []GroupAddOrDelError + Deletions []GroupAddOrDelError +} +type GroupUpdateError struct { + Action model.GroupUpdate + Error error +} +type GroupAddOrDelError struct { + Action model.Group + Error error +} + +func (actions GroupActionErrors) Amount() int { + return len(actions.Additions) + len(actions.Deletions) + len(actions.Updates) +} +func (actions GroupActionErrors) String() string { + builder := bytes.Buffer{} + for _, deletion := range actions.Deletions { + builder.WriteString(fmt.Sprintf("Deletion of group \"%s\" failed with error %s\n", deletion.Action.Email, deletion.Error.Error())) + } + for _, update := range actions.Updates { + builder.WriteString(fmt.Sprintf("Update of group \"%s\" failed with error %s\n", update.Action.After.Email, update.Error.Error())) + } + for _, addition := range actions.Additions { + builder.WriteString(fmt.Sprintf("Addition of group \"%s\" failed with error %s\n", addition.Action.Email, addition.Error.Error())) + } + return builder.String() +} + +// Commits a set of actions to a service. +// Returns all actions performed and a error if not all actions could be performed for some reason. +func (actions GroupActions) Commit(service services.UpdateService) GroupActionErrors { + + errors := GroupActionErrors{} + + if len(actions.Deletions) > 0 { + fmt.Println("(Groups) Performing deletions") + // printProgress(0, len(actions.Deletions), 0) + for _, group := range actions.Deletions { + err := service.DeleteGroup(group) + if err != nil { + // Save error + errors.Deletions = append(errors.Deletions, GroupAddOrDelError{Action: group, Error: err}) + } + } + } + + if len(actions.Additions) > 0 { + fmt.Println("(Groups) Performing additions") + // printProgress(0, len(actions.Additions), 0) + for _, group := range actions.Additions { + err := service.AddGroup(group) + if err != nil { + // Save error + errors.Additions = append(errors.Additions, GroupAddOrDelError{Action: group, Error: err}) + } + } + } + + if len(actions.Updates) > 0 { + fmt.Println("(Groups) Performing updates") + // printProgress(0, len(actions.Updates), 0) + for _, update := range actions.Updates { + err := service.UpdateGroup(update) + if err != nil { + // Save error + errors.Updates = append(errors.Updates, GroupUpdateError{Action: update, Error: err}) + } + } + } + + return errors +} + +// Determines actions required to make the "old" group list look as the "new" group list. +// Returns a list with those actions. +func GroupActionsRequired(old []model.Group, new []model.Group) GroupActions { + requiredActions := GroupActions{} + + for _, newGroup := range new { + exists := false + for _, oldGroup := range old { + // identify by Email + if newGroup.Same(oldGroup) { + // Groups exists + exists = true + // check if group has to be updates + if !newGroup.Equals(oldGroup) { + // Add group update + requiredActions.Updates = append(requiredActions.Updates, model.GroupUpdate{ + Before: oldGroup, + After: newGroup, + }) + } + break + } + } + + // Add group creation action if group doesn't exist + if !exists { + requiredActions.Additions = append(requiredActions.Additions, newGroup) + } + } + + for _, oldGroup := range old { + // check if group should be removed + removed := true + for _, newGroup := range new { + if oldGroup.Same(newGroup) { + removed = false + break + } + } + + if removed { + // Add group deletion action + requiredActions.Deletions = append(requiredActions.Deletions, oldGroup) + } + } + + return requiredActions +} diff --git a/internal/pkg/actions/user_action.go b/internal/pkg/actions/user_action.go new file mode 100644 index 0000000..ba4d29e --- /dev/null +++ b/internal/pkg/actions/user_action.go @@ -0,0 +1,141 @@ +package actions + +import ( + "bytes" + "fmt" + "github.com/cthit/goldapps/internal/pkg/model" + "github.com/cthit/goldapps/internal/pkg/services" +) + +// Set of action to be performed on a set of users +type UserActions struct { + Updates []model.UserUpdate + Additions []model.User + Deletions []model.User +} + +func (actions UserActions) Amount() int { + return len(actions.Additions) + len(actions.Deletions) + len(actions.Updates) +} + +// Set of actions that could not be performed with accompanying errors +type UserActionErrors struct { + Updates []UserUpdateError + Additions []UserAddOrDelError + Deletions []UserAddOrDelError +} +type UserUpdateError struct { + Action model.UserUpdate + Error error +} +type UserAddOrDelError struct { + Action model.User + Error error +} + +func (actions UserActionErrors) Amount() int { + return len(actions.Additions) + len(actions.Deletions) + len(actions.Updates) +} +func (actions UserActionErrors) String() string { + builder := bytes.Buffer{} + for _, deletion := range actions.Deletions { + builder.WriteString(fmt.Sprintf("Deletion of user \"%s\" failed with error %s\n", deletion.Action.Cid, deletion.Error.Error())) + } + for _, update := range actions.Updates { + builder.WriteString(fmt.Sprintf("Update of user \"%s\" failed with error %s\n", update.Action.After.Cid, update.Error.Error())) + } + for _, addition := range actions.Additions { + builder.WriteString(fmt.Sprintf("Addition of user \"%s\" failed with error %s\n", addition.Action.Cid, addition.Error.Error())) + } + return builder.String() +} + +// Commits a set of actions to a service. +// Returns all actions performed and a error if not all actions could be performed for some reason. +func (actions UserActions) Commit(service services.UpdateService) UserActionErrors { + + errors := UserActionErrors{} + + if len(actions.Deletions) > 0 { + fmt.Println("(Users) Performing deletions") + for _, user := range actions.Deletions { + err := service.DeleteUser(user) + if err != nil { + // Save error + errors.Deletions = append(errors.Deletions, UserAddOrDelError{Action: user, Error: err}) + } + } + } + + if len(actions.Updates) > 0 { + fmt.Println("(Users) Performing updates") + for _, update := range actions.Updates { + err := service.UpdateUser(update) + if err != nil { + // Save error + errors.Updates = append(errors.Updates, UserUpdateError{Action: update, Error: err}) + } + } + } + + if len(actions.Additions) > 0 { + fmt.Println("(Groups) Performing additions") + for _, user := range actions.Additions { + err := service.AddUser(user) + if err != nil { + // Save error + errors.Additions = append(errors.Additions, UserAddOrDelError{Action: user, Error: err}) + } + } + } + + return errors +} + +// Determines actions required to make the "old" user list look as the "new" user list. +// Returns a list with those actions. +func UserActionsRequired(old []model.User, new []model.User) UserActions { + requiredActions := UserActions{} + + for _, newUser := range new { + exists := false + for _, oldUser := range old { + if newUser.Same(oldUser) { + // User exists + exists = true + // check if user has to be updates + if !newUser.Equals(oldUser) { + // Add User update + requiredActions.Updates = append(requiredActions.Updates, model.UserUpdate{ + Before: oldUser, + After: newUser, + }) + } + break + } + } + + // Add user creation action if user doesn't exist + if !exists { + requiredActions.Additions = append(requiredActions.Additions, newUser) + } + } + + for _, oldUser := range old { + // check if user should be removed + removed := true + for _, newUser := range new { + if oldUser.Same(newUser) { + removed = false + break + } + } + + if removed { + // Add user deletion action + requiredActions.Deletions = append(requiredActions.Deletions, oldUser) + } + } + + return requiredActions +} diff --git a/internal/pkg/duplicates/duplicates.go b/internal/pkg/duplicates/duplicates.go new file mode 100644 index 0000000..43b0f87 --- /dev/null +++ b/internal/pkg/duplicates/duplicates.go @@ -0,0 +1,131 @@ +package duplicates + +import ( + "github.com/cthit/goldapps/internal/pkg/model" + "strings" +) + +func RemoveDuplicates(users model.Users, groups model.Groups) (model.Users, model.Groups) { + + // Compare Users with Groups + for i, user := range users { + for k := 0; k < len(groups); k++ { + // Check if any cid conflicts with any group name + if model.CompareEmails(user.Cid, extractIdentifier(groups[k].Email)) { + if groups[k].Expendable { + groups = removeArrayGroup(groups, k) + k-- // don't breaking the loop + } else { + // No good strategy exists, simply panic and let admins handle the situation + // This would probably also cause tremendous problems in other applications + panic(user.Cid + "==" + extractIdentifier(groups[k].Email)) + } + } + // Check if any user nick conflicts with any group name + if model.CompareEmails(user.Nick, extractIdentifier(groups[k].Email)) { + if groups[k].Expendable { + groups = removeArrayGroup(groups, k) + k-- // don't breaking the loop + } else { + // Nicks are not that important + users[i].Nick = "" + } + } + + for aliasIndex, alias := range groups[k].Aliases { + // Check if any cid conflicts with any group alias + if model.CompareEmails(user.Cid, extractIdentifier(alias)) { + if groups[k].Expendable { + groups[k] = removeAlias(groups[k], aliasIndex) + } else { + // No good strategy exists, simply panic and let admins handle the situation + // This would probably also cause tremendous problems in other applications + panic(user.Cid + "== (alias)" + extractIdentifier(groups[k].Email)) + } + } + // Check if any Nick conflicts with any group alias + if model.CompareEmails(user.Nick, extractIdentifier(alias)) { + if groups[k].Expendable { + groups[k] = removeAlias(groups[k], aliasIndex) + } else { + // Nicks are not that important + users[i].Nick = "" + } + } + } + } + } + + // Compare Users with Users + for i, user := range users { + for j, otherUser := range users { + // Don't check with itself + if i != j { + // Compare cids + if model.CompareEmails(user.Cid, otherUser.Cid) { + // Should not be able to happen + panic("two users with cid: " + user.Cid) + } + // Compare Nicks + if model.CompareEmails(user.Nick, otherUser.Nick) { + // Nicks are not that important + users[i].Nick = "" + users[j].Nick = "" + } + // Compare cids with nicks + if model.CompareEmails(user.Cid, otherUser.Nick) { + // Nicks are not that important + users[j].Nick = "" + } + } + } + } + + // Compare Groups with Groups + for i, group := range groups { + for j, otherGroup := range groups { + // Don't check with itself + if i != j { + // Compare Emails + if model.CompareEmails(group.Email, otherGroup.Email) { + // Something is set up wrong + panic("two groups with email: " + group.Email) + } + for _, alias := range group.Aliases { + // Compare emails with aliases + if model.CompareEmails(alias, otherGroup.Email) { + // Something is set up wrong + panic("two groups with alias/email: " + group.Email + ", " + otherGroup.Email) + } + for _, otherAlias := range otherGroup.Aliases { + // Compare aliases with aliases + if model.CompareEmails(alias, otherAlias) { + // Something is set up wrong + panic("two groups with alias: " + alias) + } + } + } + } + } + } + return users, groups +} + +func removeArrayGroup(s model.Groups, i int) model.Groups { + s[len(s)-1], s[i] = s[i], s[len(s)-1] + return s[:len(s)-1] +} + +func removeArrayString(s []string, i int) []string { + s[len(s)-1], s[i] = s[i], s[len(s)-1] + return s[:len(s)-1] +} + +func removeAlias(group model.Group, aliasIndex int) model.Group { + group.Aliases = removeArrayString(group.Aliases, aliasIndex) + return group +} + +func extractIdentifier(email string) string { + return strings.Split(email, "@")[0] +} diff --git a/internal/pkg/model/group.go b/internal/pkg/model/group.go new file mode 100644 index 0000000..914cd3f --- /dev/null +++ b/internal/pkg/model/group.go @@ -0,0 +1,76 @@ +package model + +// Represents a email group. +// Email is the id and main email for the group. +// Members is a lost of email addresses that are members of this group. +// Aliases are alternative email addresses for the group. +type Group struct { + Email string `json:"email"` + Type string `json:"type"` + Members []string `json:"members"` + Aliases []string `json:"aliases"` + Expendable bool `json:"expendable"` // Not used in comparision +} + +type Groups []Group + +// Data struct representing how a group looks not and how it should look after an update +// Allows for efficient updates as application doesn't have to re-upload whole group +type GroupUpdate struct { + Before Group + After Group +} + +// Search for groupname(email) in list of groups +func (groups Groups) Contains(email string) bool { + for _, group := range groups { + if group.Email == email { + return true + } + } + return false +} + +func (group Group) Same(other Group) bool { + return CompareEmails(group.Email, other.Email) +} + +func (group Group) Equals(other Group) bool { + if !group.Same(other) { + return false + } + + if len(group.Members) != len(other.Members) { + return false + } + for _, member := range group.Members { + contains := false + for _, otherMember := range other.Members { + if CompareEmails(member, otherMember) { + contains = true + break + } + } + if !contains { + return false + } + } + + if len(group.Aliases) != len(other.Aliases) { + return false + } + for _, alias := range group.Aliases { + contains := false + for _, otherAlias := range other.Aliases { + if CompareEmails(alias, otherAlias) { + contains = true + break + } + } + if !contains { + return false + } + } + + return true +} diff --git a/internal/pkg/model/user.go b/internal/pkg/model/user.go new file mode 100644 index 0000000..c923578 --- /dev/null +++ b/internal/pkg/model/user.go @@ -0,0 +1,61 @@ +package model + +import ( + "strings" +) + +type User struct { + Cid string `json:"cid"` + FirstName string `json:"first_name"` + SecondName string `json:"second_name"` + Nick string `json:"nick"` + Mail string `json:"mail"` +} + +type Users []User + +// Data struct representing how a user should look before and after an update +// Allows for efficient updates as application doesn't have to re-upload whole user +type UserUpdate struct { + Before User + After User +} + +// Search for username(cid) in list of groups +func (users Users) Contains(cid string) bool { + for _, user := range users { + if user.Cid == cid { + return true + } + } + return false +} + +func (user User) Same(other User) bool { + return strings.ToLower(user.Cid) == strings.ToLower(other.Cid) +} + +func (user User) Equals(other User) bool { + if !user.Same(other) { + return false + } + + if user.FirstName != other.FirstName { + return false + } + + if user.SecondName != other.SecondName { + return false + } + + if SanitizeEmail(user.Nick) != SanitizeEmail(other.Nick) { // Because google uses nick for mail + return false + } + + // Don't check email as its not saved in every services atm + /*if user.Mail != other.Mail { + return false + }*/ + + return true +} diff --git a/internal/pkg/model/util.go b/internal/pkg/model/util.go new file mode 100644 index 0000000..f6dbdfc --- /dev/null +++ b/internal/pkg/model/util.go @@ -0,0 +1,34 @@ +package model + +import ( + "regexp" + "strings" +) + +func CompareEmails(email, other string) bool { + return SanitizeEmail(email) == SanitizeEmail(other) +} + +// Only work on the part before the @ +// You are only supposed to send in the part to the left of the @ +func SanitizeEmail(s string) string { + s = strings.TrimSpace(s) + s = strings.ToLower(s) + replacelist := map[string]string{ + "π": "pi", + "å": "a", + "ä": "a", + "ö": "o", + "ø": "o", + "æ": "ae", + " ": "-", + } + allowed := regexp.MustCompile("[a-z]|[0-9]|-") + parts := strings.Split(s, "") + for i := range parts { + if !allowed.MatchString(parts[i]) { + parts[i] = replacelist[parts[i]] + } + } + return strings.Join(parts, "") +} diff --git a/internal/pkg/model/util_test.go b/internal/pkg/model/util_test.go new file mode 100644 index 0000000..f7aa51e --- /dev/null +++ b/internal/pkg/model/util_test.go @@ -0,0 +1,15 @@ +package model + +import ( + "github.com/magiconair/properties/assert" + "testing" +) + +func TestSanitizeEmail(t *testing.T) { + assert.Equal(t, SanitizeEmail("123abc"), "123abc") + assert.Equal(t, SanitizeEmail("123aBc"), "123abc") + assert.Equal(t, SanitizeEmail("123 abc"), "123-abc") + assert.Equal(t, SanitizeEmail("123-abc"), "123-abc") + assert.Equal(t, SanitizeEmail("123*abc"), "123abc") + assert.Equal(t, SanitizeEmail("123öabc"), "123oabc") +} diff --git a/internal/pkg/services/admin/password.go b/internal/pkg/services/admin/password.go new file mode 100644 index 0000000..2537d76 --- /dev/null +++ b/internal/pkg/services/admin/password.go @@ -0,0 +1,40 @@ +package admin + +import ( + "github.com/sethvargo/go-password/password" + "math/rand" + "fmt" + "google.golang.org/api/gmail/v1" + "encoding/base64" +) + +const passwordMailBody = "Action required! You are a member of a committee at the IT-section and have therefor been provided a google-account by the section. Login within the following week to setup two-factor-authentication or you might get locked out from your account. You can login on any google service such as gmail.google.com or drive.google.com with cid@chalmers.it and your provided password: %s" +const passwordMailSubject = "Login details for google services at chalmers.it" + +func newPassword() string { + numbers := rand.Intn(10) + 5 + //symbols := rand.Intn(10) + 5 + pass, err := password.Generate(64, numbers, 0, false, true) + if err != nil { + panic("Password generation failed") + } + return pass +} + +func (s googleService) sendPassword(to string, password string) error { + + from := s.admin + "@" + s.domain + body := fmt.Sprintf(passwordMailBody, password) + + msgRaw := "From: " + from + "\r\n" + + "To: " + to + "\r\n" + + "Subject: " + passwordMailSubject + "\r\n\r\n" + + body + "\r\n" + + msg := &gmail.Message{ + Raw: base64.StdEncoding.EncodeToString([]byte(msgRaw)), + } + _, err := s.mailService.Users.Messages.Send(from, msg).Do() + + return err +} diff --git a/internal/pkg/services/admin/scope.go b/internal/pkg/services/admin/scope.go new file mode 100644 index 0000000..9e85ad0 --- /dev/null +++ b/internal/pkg/services/admin/scope.go @@ -0,0 +1,16 @@ +package admin + +import ( + "google.golang.org/api/admin/directory/v1" + "google.golang.org/api/gmail/v1" +) + +func Scopes() []string { + return []string{ + admin.AdminDirectoryGroupScope, + admin.AdminDirectoryUserScope, + gmail.GmailSendScope, + } +} + +// https://www.googleapis.com/auth/admin.directory.group, https://www.googleapis.com/auth/admin.directory.user diff --git a/internal/pkg/services/admin/service.go b/internal/pkg/services/admin/service.go new file mode 100644 index 0000000..2222ae8 --- /dev/null +++ b/internal/pkg/services/admin/service.go @@ -0,0 +1,70 @@ +package admin + +import ( + "github.com/cthit/goldapps/internal/pkg/services" + + "google.golang.org/api/admin/directory/v1" // Imports as admin + "google.golang.org/api/gmail/v1" // Imports as gmail + + "golang.org/x/net/context" + "golang.org/x/oauth2/google" + + "io/ioutil" + "strings" +) + +const googleDuplicateEntryError = "googleapi: Error 409: Entity already exists., duplicate" + +// my_customer seems to work... +const googleCustomer = "my_customer" + +type googleService struct { + adminService *admin.Service + mailService *gmail.Service + admin string + domain string +} + +func NewGoogleService(keyPath string, adminMail string) (services.UpdateService, error) { + + jsonKey, err := ioutil.ReadFile(keyPath) + if err != nil { + return nil, err + } + + // Parse jsonKey + config, err := google.JWTConfigFromJSON(jsonKey, Scopes()...) + if err != nil { + return nil, err + } + + // Why do I need this?? + config.Subject = adminMail + + // Create a http client + client := config.Client(context.Background()) + + service, err := admin.New(client) + if err != nil { + return nil, err + } + + mailService, err := gmail.New(client) + if err != nil { + return nil, err + } + + // Extract account and mail + s := strings.Split(adminMail, "@") + admin := s[0] + domain := s[1] + + gs := googleService{ + adminService: service, + mailService: mailService, + admin: admin, + domain: domain, + } + + return gs, nil +} diff --git a/internal/pkg/services/admin/service_groups.go b/internal/pkg/services/admin/service_groups.go new file mode 100644 index 0000000..58bdadd --- /dev/null +++ b/internal/pkg/services/admin/service_groups.go @@ -0,0 +1,203 @@ +package admin + +import ( + "bytes" + "fmt" + "github.com/cthit/goldapps/internal/pkg/model" + "google.golang.org/api/admin/directory/v1" // Imports as admin + "time" +) + +func (s googleService) DeleteGroup(group model.Group) error { + err := s.adminService.Groups.Delete(group.Email).Do() + return err +} + +func (s googleService) UpdateGroup(groupUpdate model.GroupUpdate) error { + newGroup := admin.Group{ + Email: groupUpdate.Before.Email, + } + + // Add all new members + for _, member := range groupUpdate.After.Members { + exists := false + for _, existingMember := range groupUpdate.Before.Members { + if model.CompareEmails(member, existingMember) { + exists = true + break + } + } + if !exists { + _, err := s.adminService.Members.Insert(groupUpdate.Before.Email, &admin.Member{Email: member}).Do() + if err != nil { + fmt.Printf("Failed to add menber %s\n", member) + return err + } + } + } + + // Remove all old members + for _, existingMember := range groupUpdate.Before.Members { + keep := false + for _, member := range groupUpdate.After.Members { + if model.CompareEmails(existingMember, member) { + keep = true + break + } + } + if !keep { + err := s.adminService.Members.Delete(groupUpdate.Before.Email, existingMember).Do() + if err != nil { + return err + } + } + } + + // Add all new aliases + for _, alias := range groupUpdate.After.Aliases { + exists := false + for _, existingAlias := range groupUpdate.Before.Aliases { + if model.CompareEmails(alias, existingAlias) { + exists = true + break + } + } + if !exists { + _, err := s.adminService.Groups.Aliases.Insert(groupUpdate.Before.Email, &admin.Alias{Alias: alias}).Do() + if err != nil { + return err + } + } + } + + // Remove all old aliases + for _, existingAlias := range groupUpdate.Before.Aliases { + keep := false + for _, alias := range groupUpdate.After.Aliases { + if model.CompareEmails(existingAlias, alias) { + keep = true + break + } + } + if !keep { + err := s.adminService.Groups.Aliases.Delete(groupUpdate.Before.Email, existingAlias).Do() + if err != nil { + return err + } + } + } + + _, err := s.adminService.Groups.Update(groupUpdate.Before.Email, &newGroup).Do() + return err +} + +func (s googleService) AddGroup(group model.Group) error { + newGroup := admin.Group{ + Email: group.Email, + } + + _, err := s.adminService.Groups.Insert(&newGroup).Do() + if err != nil { + return err + } + + time.Sleep(time.Second * 10) + + // Add members + for _, member := range group.Members { + _, err := s.adminService.Members.Insert(group.Email, &admin.Member{Email: member}).Do() + if err != nil { + return err + } + } + + // Add Aliases + for _, alias := range group.Aliases { + _, err := s.adminService.Groups.Aliases.Insert(group.Email, &admin.Alias{Alias: alias}).Do() + if err != nil { + return err + } + } + return nil +} + +func (s googleService) GetGroups() ([]model.Group, error) { + + adminGroups, err := s.getGoogleGroups(googleCustomer) + if err != nil { + return nil, err + } + + groups := make([]model.Group, len(adminGroups)) + for i, group := range adminGroups { + + p := (i * 100) / len(groups) + + builder := bytes.Buffer{} + for i := 0; i < 100; i++ { + if i < p { + builder.WriteByte('=') + } else if i == p { + builder.WriteByte('>') + } else { + builder.WriteByte(' ') + } + + } + + fmt.Printf("\rProgress: [%s] %d/%d", builder.String(), i+1, len(groups)) + + members, err := s.getGoogleGroupMembers(group.Email) + if err != nil { + return nil, err + } + + groups[i] = model.Group{ + Email: group.Email, + Members: members, + Aliases: group.Aliases, + } + } + fmt.Printf("\rDone\n") + + return groups, nil + +} + +func (s googleService) getGoogleGroups(customer string) ([]admin.Group, error) { + groups, err := s.adminService.Groups.List().Customer(customer).Do() + if err != nil { + return nil, err + } + + for groups.NextPageToken != "" { + newGroups, err := s.adminService.Groups.List().Customer(customer).PageToken(groups.NextPageToken).Do() + if err != nil { + return nil, err + } + + groups.Groups = append(groups.Groups, newGroups.Groups...) + groups.NextPageToken = newGroups.NextPageToken + } + + result := make([]admin.Group, len(groups.Groups)) + for i, group := range groups.Groups { + result[i] = *group + } + + return result, nil +} + +func (s googleService) getGoogleGroupMembers(email string) ([]string, error) { + members, err := s.adminService.Members.List(email).Do() + if err != nil { + return nil, err + } + + result := make([]string, len(members.Members)) + for i, member := range members.Members { + result[i] = member.Email + } + + return result, nil +} diff --git a/internal/pkg/services/admin/service_users.go b/internal/pkg/services/admin/service_users.go new file mode 100644 index 0000000..0cbf6b5 --- /dev/null +++ b/internal/pkg/services/admin/service_users.go @@ -0,0 +1,133 @@ +package admin + +import ( + "fmt" + "github.com/cthit/goldapps/internal/pkg/model" + "google.golang.org/api/admin/directory/v1" // Imports as admin + "strings" + "time" +) + +func (s googleService) AddUser(user model.User) error { + + usr := buildGoldappsUser(user, s.domain) + + password := newPassword() + + usr.Password = password + usr.ChangePasswordAtNextLogin = true + + _, err := s.adminService.Users.Insert(usr).Do() + if err != nil { + return err + } + + err = s.sendPassword(user.Mail, password) + if err != nil { + return err + } + + // Google needs time for the addition to propagate + time.Sleep(time.Second) + + // Add alias for nick@example.ex + return s.addUserAlias(fmt.Sprintf("%s@%s", model.SanitizeEmail(user.Cid), s.domain), fmt.Sprintf("%s@%s", model.SanitizeEmail(user.Nick), s.domain)) +} + +func (s googleService) UpdateUser(update model.UserUpdate) error { + _, err := s.adminService.Users.Update( + fmt.Sprintf("%s@%s", model.SanitizeEmail(update.Before.Cid), s.domain), + buildGoldappsUser(update.After, s.domain), + ).Do() + if err != nil { + return err + } + + // Add alias for nick@example.ex + return s.addUserAlias(fmt.Sprintf("%s@%s", model.SanitizeEmail(update.After.Cid), s.domain), fmt.Sprintf("%s@%s", model.SanitizeEmail(update.After.Nick), s.domain)) +} + +func (s googleService) DeleteUser(user model.User) error { + admin := fmt.Sprintf("%s@%s", s.admin, s.domain) + userId := fmt.Sprintf("%s@%s", model.SanitizeEmail(user.Cid), s.domain) + if admin == userId { + fmt.Printf("Skipping andmin user: %s\n", admin) + } + + err := s.adminService.Users.Delete(userId).Do() + return err +} + +func (s googleService) GetUsers() ([]model.User, error) { + adminUsers, err := s.getGoogleUsers(googleCustomer) + if err != nil { + return nil, err + } + users := make([]model.User, len(adminUsers)-1) + + admin := fmt.Sprintf("%s@%s", s.admin, s.domain) + + i := 0 + for _, adminUser := range adminUsers { + if admin != adminUser.PrimaryEmail { // Don't list admin account + // Separating nick and firstName from (Nick / FirstName) + givenName := strings.Split(adminUser.Name.GivenName, " / ") + nick := givenName[0] + firstName := "" + if len(givenName) >= 2 { + firstName = givenName[1] + } + + // Extracting cid form (cid@example.ex) + cid := strings.Split(adminUser.PrimaryEmail, "@")[0] + + users[i] = model.User{ + Cid: cid, + FirstName: firstName, + SecondName: adminUser.Name.FamilyName, + Nick: nick, + } + i++ + } + } + + return users, err +} + +func (s googleService) getGoogleUsers(customer string) ([]admin.User, error) { + users, err := s.adminService.Users.List().Customer(customer).Do() + if err != nil { + return nil, err + } + + for users.NextPageToken != "" { + newUsers, err := s.adminService.Users.List().Customer(customer).PageToken(users.NextPageToken).Do() + if err != nil { + return nil, err + } + + users.Users = append(users.Users, newUsers.Users...) + users.NextPageToken = newUsers.NextPageToken + } + + result := make([]admin.User, len(users.Users)) + for i, user := range users.Users { + result[i] = *user + } + + return result, nil +} + +func (s googleService) addUserAlias(userKey string, alias string) error { + _, err := s.adminService.Users.Aliases.Insert(userKey, &admin.Alias{ + Alias: alias, + }).Do() + if err != nil { + if err.Error() == googleDuplicateEntryError { + fmt.Printf("Warning: Could not add alias for %s. It already exists. \n", alias) + } else { + return err + } + } + return nil +} diff --git a/internal/pkg/services/admin/util.go b/internal/pkg/services/admin/util.go new file mode 100644 index 0000000..ada3ada --- /dev/null +++ b/internal/pkg/services/admin/util.go @@ -0,0 +1,18 @@ +package admin + +import ( + "fmt" + "github.com/cthit/goldapps/internal/pkg/model" + "google.golang.org/api/admin/directory/v1" // Imports as admin +) + +func buildGoldappsUser(user model.User, domain string) *admin.User { + return &admin.User{ + Name: &admin.UserName{ + FamilyName: user.SecondName, + GivenName: fmt.Sprintf("%s / %s", user.Nick, user.FirstName), + }, + IncludeInGlobalAddressList: true, + PrimaryEmail: fmt.Sprintf("%s@%s", model.SanitizeEmail(user.Cid), domain), + } +} diff --git a/internal/pkg/services/json/service.go b/internal/pkg/services/json/service.go new file mode 100644 index 0000000..345226c --- /dev/null +++ b/internal/pkg/services/json/service.go @@ -0,0 +1,208 @@ +package json + +import ( + "encoding/json" + "fmt" + "github.com/cthit/goldapps/internal/pkg/model" + "io/ioutil" + "os" +) + +type Service struct { + path string +} + +type dataObject struct { + Groups []model.Group `json:"groups"` + Users []model.User `json:"users"` +} + +func (s Service) DeleteUser(user model.User) error { + groups, err := s.GetGroups() + if err != nil { + return err + } + users, err := s.GetUsers() + if err != nil { + return err + } + + for i, u := range users { + if u.Cid == user.Cid { + err = s.save(dataObject{ + groups, + append(users[:i], users[i+1:]...), + }) + return err + } + } + return fmt.Errorf("user not found %v", user) +} + +func (s Service) UpdateUser(update model.UserUpdate) error { + groups, err := s.GetGroups() + if err != nil { + return err + } + users, err := s.GetUsers() + if err != nil { + return err + } + + for i, u := range users { + if u.Cid == update.Before.Cid { + err = s.save(dataObject{ + groups, + append(append(users[:i], update.After), users[i+1:]...), + }) + return err + } + } + return fmt.Errorf("user not found %v", update.Before) +} + +func (s Service) AddUser(user model.User) error { + groups, err := s.GetGroups() + if err != nil { + return err + } + users, err := s.GetUsers() + if err != nil { + return err + } + + users = append(users, user) + + err = s.save(dataObject{ + groups, + users, + }) + return err +} + +func (s Service) GetUsers() ([]model.User, error) { + + data, err := s.get() + if err != nil { + return nil, err + } + + return data.Users, nil +} + +func NewJsonService(path string) (Service, error) { + + // Check if file exists + _, err := os.Stat(path) + if os.IsNotExist(err) { + // Create file + _, err := os.Create("path") + if err != nil { + return Service{}, err + } + // Write empty object to file + err = Service{path: path}.save(dataObject{}) + if err != nil { + return Service{}, err + } + } + + return Service{ + path: path, + }, nil +} + +func (s Service) save(data dataObject) error { + json, _ := json.Marshal(data) + + err := ioutil.WriteFile(s.path, json, 0666) + return err +} + +func (s Service) get() (dataObject, error) { + + bytes, err := ioutil.ReadFile(s.path) + if err != nil { + return dataObject{}, err + } + + var data dataObject + err = json.Unmarshal(bytes, &data) + if err != nil { + return dataObject{}, err + } + + return data, nil +} + +func (s Service) DeleteGroup(group model.Group) error { + groups, err := s.GetGroups() + if err != nil { + return err + } + users, err := s.GetUsers() + if err != nil { + return err + } + + for i, g := range groups { + if g.Email == group.Email { + err = s.save(dataObject{append(groups[:i], groups[i+1:]...), + users, + }) + return err + } + } + return fmt.Errorf("group not found %v", group) +} + +func (s Service) UpdateGroup(groupUpdate model.GroupUpdate) error { + groups, err := s.GetGroups() + if err != nil { + return err + } + users, err := s.GetUsers() + if err != nil { + return err + } + + for i, g := range groups { + if g.Email == groupUpdate.Before.Email { + err = s.save(dataObject{ + append(append(groups[:i], groupUpdate.After), groups[i+1:]...), + users, + }) + return err + } + } + return fmt.Errorf("group not found %v", groupUpdate.Before) +} + +func (s Service) AddGroup(group model.Group) error { + groups, err := s.GetGroups() + if err != nil { + return err + } + users, err := s.GetUsers() + if err != nil { + return err + } + + groups = append(groups, group) + + err = s.save(dataObject{ + groups, + users, + }) + return err +} + +func (s Service) GetGroups() ([]model.Group, error) { + + data, err := s.get() + if err != nil { + return nil, err + } + + return data.Groups, nil +} diff --git a/internal/pkg/services/ldap/service.go b/internal/pkg/services/ldap/service.go new file mode 100644 index 0000000..e62b4f1 --- /dev/null +++ b/internal/pkg/services/ldap/service.go @@ -0,0 +1,529 @@ +package ldap + +import ( + "crypto/tls" + + "fmt" + "github.com/cthit/goldapps/internal/pkg/model" + "gopkg.in/ldap.v2" + "strings" +) + +type ServiceLDAP struct { + Connection *ldap.Conn + DBConfig ServerConfig + GroupsConfig EntryConfig + UsersConfig EntryConfig + CustomEntryConfigs []CustomEntryConfig +} + +type ServerConfig struct { + Url string + ServerName string +} + +type EntryConfig struct { + BaseDN string + Filter string + Attributes []string +} + +type CustomEntryConfig struct { + BaseDN string + Filter string + ParentFilter string + Attributes []string + Mail string +} + +type LoginConfig struct { + UserName string + Password string +} + +func NewLDAPService(dbConfig ServerConfig, login LoginConfig, usersConfig EntryConfig, groupsConfig EntryConfig, customEntryConfigs []CustomEntryConfig) (*ServiceLDAP, error) { + + l, err := ldap.DialTLS("tcp", dbConfig.Url, &tls.Config{ServerName: dbConfig.ServerName}) + if err != nil { + return nil, err + } + // FIXME: Close connection on garbage collection + //defer l.Close() + + err = l.Bind(login.UserName, login.Password) + if err != nil { + return nil, err + } + + ld := &ServiceLDAP{ + Connection: l, + DBConfig: dbConfig, + UsersConfig: usersConfig, + GroupsConfig: groupsConfig, + CustomEntryConfigs: customEntryConfigs, + } + + return ld, nil + +} + +// Collects all users from LDAP as a slice of *ldap.Entry's +func (s ServiceLDAP) users() ([]*ldap.Entry, error) { + searchRequest := ldap.NewSearchRequest( + s.UsersConfig.BaseDN, // The base dn to search + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, + s.UsersConfig.Filter, // The filter to apply + s.UsersConfig.Attributes, // A list attributes to retrieve + nil, + ) + + result, err := s.Connection.Search(searchRequest) + if err != nil { + return nil, err + } + + return result.Entries, nil +} + +func (s ServiceLDAP) GetUsers() ([]model.User, error) { + return s.getUsers() +} + +// Collect all users who are members of a committee +func (s ServiceLDAP) getUsers() ([]model.User, error) { + users, err := s.users() + if err != nil { + return nil, err + } + + // Create a search request to collect all groups from LDAP + searchRequest := ldap.NewSearchRequest( + s.GroupsConfig.BaseDN, // The base dn to search + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, + s.GroupsConfig.Filter, // The filter to apply + s.GroupsConfig.Attributes, // A list attributes to retrieve + nil, + ) + + // Collect the group entries + groups, err := s.Connection.Search(searchRequest) + if err != nil { + return nil, err + } + + // Create an empty model.Group slice + privilegedUsers := make(model.Users, 0) + + for _, group := range groups.Entries { + // TODO: What qualified as a privileged group should be made configurable. See FIXME:s + if group.GetAttributeValue("type") != "Committee" /* FIXME */ { + continue // Only Committees are considered privileged groups + } + + cn := group.GetAttributeValue("cn") + // Check if RDN is the same as the groups parent. FIXME + if strings.HasPrefix(group.DN, fmt.Sprintf("cn=%s,ou=%s", cn, cn)) { + for _, member := range group.GetAttributeValues("member") { + for _, user := range parsePrivilegedGroupMember(member, users, groups.Entries) { + if !privilegedUsers.Contains(user.GetAttributeValue("uid")) { + if user.GetAttributeValue("gdprEducated") == "TRUE" { // only add user if he's gdpr educated + privilegedUsers = append(privilegedUsers, model.User{ + Cid: user.GetAttributeValue("uid"), + Nick: user.GetAttributeValue("nickname"), + FirstName: user.GetAttributeValue("givenName"), + SecondName: user.GetAttributeValue("sn"), + Mail: user.GetAttributeValue("mail"), + }) + } + } + } + } + } + } + + return privilegedUsers, nil +} + +// Recursively parse member tree and return users +func parsePrivilegedGroupMember(memberDN string, users []*ldap.Entry, groups []*ldap.Entry) []*ldap.Entry { + res := make([]*ldap.Entry, 0) + if dnIsUser(memberDN) { + for _, user := range users { + if user.DN == memberDN { + res = append(res, user) + break + } + } + } else { + for _, group := range groups { + if group.DN == memberDN { + for _, subMember := range group.GetAttributeValues("member") { + res = append(res, parsePrivilegedGroupMember(subMember, users, groups)...) + } + break + } + } + } + return res +} + +// Collects all committees from LDAP and then creates a +// model.Group slice. +func (s ServiceLDAP) GetGroups() ([]model.Group, error) { + users, err := s.users() + if err != nil { + return nil, err + } + + // Creates a search request to collect all committees from LDAP + searchRequest := ldap.NewSearchRequest( + s.GroupsConfig.BaseDN, // The base dn to search + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, + s.GroupsConfig.Filter, // The filter to apply + s.GroupsConfig.Attributes, // A list attributes to retrieve + nil, + ) + + // Collects the committee entries + committees, err := s.Connection.Search(searchRequest) + if err != nil { + return nil, err + } + + // Creates an empty model.Group slice + groups := make([]model.Group, 0) + + // Creates a model.Group with appropriate mails and members + for _, entry := range committees.Entries { + + // Creates a model.Group with it's mail + committee := model.Group{ + Email: entry.GetAttributeValue("mail"), + Type: entry.GetAttributeValue("type"), + Members: nil, + } + + if committee.Email == "" { + continue + } + + // Creates an empty members slice + members := make([]string, 0) // len(users) might break if we have all users and some groups in the members field + + // Fills the members slice with data + for _, member := range entry.GetAttributeValues("member") { + var m *ldap.Entry + + if dnIsUser(member) { + m = findEntry(users, member) + } else { + m = findEntry(committees.Entries, member) + } + + if m != nil { + mail := m.GetAttributeValue("mail") + if mail != "" { + members = append(members, mail) + } + } + } + + committee.Members = members + + groups = append(groups, committee) + } + + customGroups, err := s.GetCustomGroups() + if err != nil { + return nil, err + } + groups = append(groups, customGroups...) + + positionGroups, err := s.getPositionGroups() + if err != nil { + return nil, err + } + groups = append(groups, positionGroups...) + + chairmenGroupMembers, err := s.getRoleInGroups("ordf", false) + if err != nil { + return nil, err + } + groups = append(groups, model.Group{ + Email: "ordforanden@chalmers.it", + Members: chairmenGroupMembers, + }) + + chairmenInCommitteesGroupMembers, err := s.getRoleInGroups("ordf", true) + if err != nil { + return nil, err + } + groups = append(groups, model.Group{ + Email: "ordforanden.kommitteer@chalmers.it", + Members: chairmenInCommitteesGroupMembers, + }) + + treasurersGroupMembers, err := s.getRoleInGroups("kassor", false) + if err != nil { + return nil, err + } + groups = append(groups, model.Group{ + Email: "kassorer@chalmers.it", + Members: treasurersGroupMembers, + }) + + treasurersInCommitteesGroupMembers, err := s.getRoleInGroups("kassor", true) + if err != nil { + return nil, err + } + groups = append(groups, model.Group{ + Email: "kassorer.kommitteer@chalmers.it", + Members: treasurersInCommitteesGroupMembers, + }) + + accounts, err := s.getUsers() + if err != nil { + return nil, err + } + + for i, group := range groups { + if group.Type == "Committee" { + for _, comitteeGroupMemberEmail := range group.Members { + + for j := range groups { + if groups[j].Email == comitteeGroupMemberEmail { + groups[j] = replaceWithAccountEmail(groups[j], accounts) + } + } + } + } else if groups[i].Type == "CommitteeDirect" { + groups[i] = replaceWithAccountEmail(groups[i], accounts) + } + } + + return groups, nil +} + +func replaceWithAccountEmail(group model.Group, users model.Users) model.Group { + for i := 0; i < len(group.Members); i++ { + replacementFound := false + for _, user := range users { + if user.Mail == group.Members[i] { + replacementFound = true + group.Members[i] = user.Cid + "@chalmers.it" + } + } + if !replacementFound { + fmt.Printf("WARNING: no replacement could be found for %s in %s \n", group.Members[i], group.Email) + + //Remove member + group.Members = append(group.Members[:i], group.Members[i+1:]...) + i-- + } + } + return group +} + +func (s ServiceLDAP) GetCustomGroups() ([]model.Group, error) { + users, err := s.users() + if err != nil { + return nil, err + } + + customGroups := make([]model.Group, 0) + + for _, entry := range s.CustomEntryConfigs { + // Creates a search request to collect all committees from LDAP + searchRequest := ldap.NewSearchRequest( + entry.BaseDN, // The base dn to search + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, + entry.Filter, // The filter to apply + entry.Attributes, // A list attributes to retrieve + nil, + ) + + result, err := s.Connection.Search(searchRequest) + if err != nil { + return nil, err + } + + members := make([]string, 0) // len(users) might break if we have all users and some groups in the members field + + for _, member := range result.Entries { + + var parentResult *ldap.SearchResult = nil + if entry.ParentFilter != "" { + parentSearchRequest := ldap.NewSearchRequest( + entry.BaseDN, // The base dn to search + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, + // FIXME: The %childRDN% is only necessary since year groups (e.g. snit14) are the same type as their Committee/Society. + strings.Replace(entry.ParentFilter, "%childRDN%", getRDN(member.DN), -1), // The filter to apply + entry.Attributes, // A list attributes to retrieve + nil, + ) + + parentResult, err = s.Connection.Search(parentSearchRequest) + if err != nil { + return nil, err + } + } + + // If parent filter exists: check if member has a parent that matches + var addMember = parentResult == nil + if !addMember { + for _, parent := range parentResult.Entries { + if dnIsParentOf(parent.DN, member.DN) { + addMember = true + break + } + } + } + + if addMember { + mail := member.GetAttributeValue("mail") + localMembers := member.GetAttributeValues("member") + // Check if the found entry has a mail associated with it + if mail == "" { // if not it should have members which do + for _, localMember := range localMembers { + mail = findEntry(users, localMember).GetAttributeValue("mail") + //fmt.Println(mail) + members = append(members, mail) + } + } else { + mail := member.GetAttributeValue("mail") + if mail != "" { + members = append(members, mail) + } + } + + } + } + + group := model.Group{ + Email: entry.Mail, + Members: members, + } + + customGroups = append(customGroups, group) + } + + return customGroups, nil +} + +func (s ServiceLDAP) getPositionGroups() ([]model.Group, error) { + users, err := s.users() + if err != nil { + return nil, err + } + + var positionGroups []model.Group + + searchRequest := ldap.NewSearchRequest( + "ou=fkit,ou=groups,dc=chalmers,dc=it", // The base dn to search + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, + "(&(objectClass=itPosition))", // The filter to apply + []string{"cn", "member"}, // A list attributes to retrieve + nil, + ) + + result, err := s.Connection.Search(searchRequest) + if err != nil { + return nil, err + } + + for _, entry := range result.Entries { + groupType, err := dnPositionType(s, entry.DN) + DnSplit := strings.SplitN(entry.DN, ",", 3) + pos := DnSplit[0][3:] + grp := DnSplit[1][3:] + if err != nil { + return nil, err + } + + posGroup := model.Group{ + Email: fmt.Sprintf("%s.%s@chalmers.it", pos, grp), + Type: groupType + "Direct", + } + + for _, member := range entry.GetAttributeValues("member") { + ent := findEntry(users, member) + // TODO detta krashar om användaren ej finns + mail := ent.GetAttributeValue("mail") + posGroup.Members = append(posGroup.Members, mail) + } + + positionGroups = append(positionGroups, posGroup) + + } + return positionGroups, nil + +} + +func (s ServiceLDAP) getRoleInGroups(role string, onlyCommittees bool) ([]string, error) { + searchRequest := ldap.NewSearchRequest( + "ou=fkit,ou=groups,dc=chalmers,dc=it", // The base dn to search + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, + fmt.Sprintf("(&(objectClass=itPosition)(cn=%s))", role), // The filter to apply + []string{"cn"}, // A list attributes to retrieve + nil, + ) + + result, err := s.Connection.Search(searchRequest) + if err != nil { + return nil, err + } + + var treasurersInCommitteeGroup []string + + for _, entry := range result.Entries { + gtype, err := dnPositionType(s, entry.DN) + if err != nil { + return nil, err + } + if !onlyCommittees || gtype == "Committee" { + dnSplit := strings.SplitN(entry.DN, ",", 3) + treasurersInCommitteeGroup = append(treasurersInCommitteeGroup, fmt.Sprintf("%s.%s@chalmers.it", role, dnSplit[1][3:])) + } + + } + return treasurersInCommitteeGroup, nil +} + +func findEntry(ldapEntries []*ldap.Entry, DN string) *ldap.Entry { + for _, entry := range ldapEntries { + if entry.DN == DN { + return entry + } + } + return nil +} + +func getRDN(DN string) string { + return strings.Split(strings.Split(DN, ",")[0], "=")[1] +} + +func dnIsParentOf(parent string, node string) bool { + return len(parent) != len(node) && strings.Contains(node, parent) +} + +func dnIsUser(DN string) bool { + return len(DN) >= 4 && DN[0:4] == "uid=" +} + +func dnPositionType(s ServiceLDAP, DN string) (string, error) { + newDN := strings.SplitN(DN, ",", 2)[1] // Creates the dn for the group + sr := ldap.NewSearchRequest( + newDN, // The base dn to search + ldap.ScopeBaseObject, ldap.NeverDerefAliases, 0, 0, false, + "(&(objectClass=*))", // The filter to apply + []string{"type"}, // A list attributes to retrieve + nil, + ) + + result, err := s.Connection.Search(sr) + if err != nil { + return "", err + } + + return result.Entries[0].GetAttributeValue("type"), nil +} diff --git a/internal/pkg/services/services.go b/internal/pkg/services/services.go new file mode 100644 index 0000000..a46a44f --- /dev/null +++ b/internal/pkg/services/services.go @@ -0,0 +1,20 @@ +package services + +import ( + "github.com/cthit/goldapps/internal/pkg/model" +) + +type CollectionService interface { + GetGroups() ([]model.Group, error) + GetUsers() ([]model.User, error) +} + +type UpdateService interface { + DeleteGroup(model.Group) error + UpdateGroup(model.GroupUpdate) error + AddGroup(model.Group) error + DeleteUser(model.User) error + UpdateUser(model.UserUpdate) error + AddUser(model.User) error + CollectionService +} diff --git a/json/service.go b/json/service.go deleted file mode 100644 index 3943539..0000000 --- a/json/service.go +++ /dev/null @@ -1,83 +0,0 @@ -package json - -import ( - "github.com/cthit/goldapps" - "encoding/json" - "io/ioutil" - "fmt" -) - -type jsonService struct { - path string -} - -func NewJsonService(path string) (jsonService, error) { - return jsonService{ - path: path, - }, nil -} - -func (s jsonService) save(groups []goldapps.Group) error { - data, _ := json.Marshal(groups) - - err := ioutil.WriteFile(s.path, data, 0666) - return err -} - -func (s jsonService) DeleteGroup(group goldapps.Group) error { - groups, err := s.GetGroups() - if err != nil { - return err - } - - for i,g := range groups { - if g.Email == group.Email{ - err = s.save(append(groups[:i], groups[i+1:]...)) - return err - } - } - return fmt.Errorf("group not found %v", group) -} - -func (s jsonService) UpdateGroup(groupUpdate goldapps.GroupUpdate) error { - groups, err := s.GetGroups() - if err != nil { - return err - } - - for i,g := range groups { - if g.Email == groupUpdate.Before.Email{ - err = s.save(append(append(groups[:i], groupUpdate.After), groups[i+1:]...)) - return err - } - } - return fmt.Errorf("group not found %v", groupUpdate.Before) -} - -func (s jsonService) AddGroup(group goldapps.Group) error { - groups, err := s.GetGroups() - if err != nil { - return err - } - - groups = append(groups, group) - - err = s.save(groups) - return err -} - -func (s jsonService) GetGroups() ([]goldapps.Group, error) { - - bytes, err := ioutil.ReadFile(s.path) - if err != nil { - return nil, err - } - - var data []goldapps.Group - err = json.Unmarshal(bytes, &data) - if err != nil { - return nil, err - } - - return data, nil -} diff --git a/ldap/service.go b/ldap/service.go deleted file mode 100644 index c930b79..0000000 --- a/ldap/service.go +++ /dev/null @@ -1,245 +0,0 @@ -package ldap - -import ( - "crypto/tls" - - "github.com/cthit/goldapps" - "gopkg.in/ldap.v2" - "strings" -) - -type ServiceLDAP struct { - Connection *ldap.Conn - DBConfig ServerConfig - GroupsConfig EntryConfig - UsersConfig EntryConfig - CustomEntryConfigs []CustomEntryConfig -} - -type ServerConfig struct { - Url string - ServerName string -} - -type EntryConfig struct { - BaseDN string - Filter string - Attributes []string -} - -type CustomEntryConfig struct { - BaseDN string - Filter string - ParentFilter string - Attributes []string - Mail string -} - -type LoginConfig struct { - UserName string - Password string -} - -func NewLDAPService(dbConfig ServerConfig, login LoginConfig, usersConfig EntryConfig, groupsConfig EntryConfig, customEntryConfigs []CustomEntryConfig) (*ServiceLDAP, error) { - - l, err := ldap.DialTLS("tcp", dbConfig.Url, &tls.Config{ServerName: dbConfig.ServerName}) - if err != nil { - return nil, err - } - // FIXME: Close connection on garbage collection - //defer l.Close() - - err = l.Bind(login.UserName, login.Password) - if err != nil { - return nil, err - } - - ld := &ServiceLDAP{ - Connection: l, - DBConfig: dbConfig, - UsersConfig: usersConfig, - GroupsConfig: groupsConfig, - CustomEntryConfigs: customEntryConfigs, - } - - return ld, nil - -} - -// Collects all users from LDAP as a slice of *ldap.Entry's -func (s ServiceLDAP) users() ([]*ldap.Entry, error) { - searchRequest := ldap.NewSearchRequest( - s.UsersConfig.BaseDN, // The base dn to search - ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, - s.UsersConfig.Filter, // The filter to apply - s.UsersConfig.Attributes, // A list attributes to retrieve - nil, - ) - - result, err := s.Connection.Search(searchRequest) - if err != nil { - return nil, err - } - - return result.Entries, nil -} - -// Collects all committees from LDAP and then creates a -// goldapps.Group slice. -func (s ServiceLDAP) GetGroups() ([]goldapps.Group, error) { - users, err := s.users() - if err != nil { - return nil, err - } - - // Creates a search request to collect all committees from LDAP - searchRequest := ldap.NewSearchRequest( - s.GroupsConfig.BaseDN, // The base dn to search - ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, - s.GroupsConfig.Filter, // The filter to apply - s.GroupsConfig.Attributes, // A list attributes to retrieve - nil, - ) - - // Collects the committee entries - committees, err := s.Connection.Search(searchRequest) - if err != nil { - return nil, err - } - - // Creates an empty goldapps.Group slice - groups := make([]goldapps.Group, 0) - - // Creates a goldapps.Group with appropriate mails and members - for _, entry := range committees.Entries { - - // Creates a goldapps.Group with it's mail - committee := goldapps.Group{ - Email: entry.GetAttributeValue("mail"), - Members: nil, - } - - // Creates an empty members slice - members := make([]string, 0) // len(users) might break if we have all users and some groups in the members field - - // Fills the members slice with data - for _, member := range entry.GetAttributeValues("member") { - var m *ldap.Entry - - if dnIsUser(member) { - m = findEntry(users, member) - } else { - m = findEntry(committees.Entries, member) - } - - if m != nil { - mail := m.GetAttributeValue("mail") - if mail != "" { - members = append(members, mail) - } - } - } - - committee.Members = members - - groups = append(groups, committee) - } - - customGroups, err := s.GetCustomGroups() - if err != nil { - return nil, err - } - - groups = append(groups, customGroups...) - - return groups, nil -} - -func (s ServiceLDAP) GetCustomGroups() ([]goldapps.Group, error) { - customGroups := make([]goldapps.Group, 0) - - for _, entry := range s.CustomEntryConfigs { - // Creates a search request to collect all committees from LDAP - searchRequest := ldap.NewSearchRequest( - entry.BaseDN, // The base dn to search - ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, - entry.Filter, // The filter to apply - entry.Attributes, // A list attributes to retrieve - nil, - ) - - result, err := s.Connection.Search(searchRequest) - if err != nil { - return nil, err - } - - members := make([]string, 0) // len(users) might break if we have all users and some groups in the members field - - for _, member := range result.Entries { - - var parentResult *ldap.SearchResult = nil - if entry.ParentFilter != "" { - parentSearchRequest := ldap.NewSearchRequest( - entry.BaseDN, // The base dn to search - ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, - strings.Replace(entry.ParentFilter, "%childRDN%", getRDN(member.DN), -1), // The filter to apply - entry.Attributes, // A list attributes to retrieve - nil, - ) - - parentResult, err = s.Connection.Search(parentSearchRequest) - if err != nil { - return nil, err - } - } - - // If parent filter exists: check if member has a parent that matches - var addMember = parentResult == nil - if !addMember { - for _, parent := range parentResult.Entries { - if dnIsParentOf(parent.DN, member.DN) { - addMember = true - break - } - } - } - - if addMember { - mail := member.GetAttributeValue("mail") - if mail != "" { - members = append(members, mail) - } - } - } - - group := goldapps.Group{ - Email: entry.Mail, - Members: members, - } - - customGroups = append(customGroups, group) - } - - return customGroups, nil -} - -func findEntry(ldapEntries []*ldap.Entry, DN string) *ldap.Entry { - for _, entry := range ldapEntries { - if entry.DN == DN { - return entry - } - } - return nil -} - -func getRDN(DN string) string { - return strings.Split(strings.Split(DN, ",")[0], "=")[1] -} - -func dnIsParentOf(parent string, node string) bool { - return len(parent) != len(node) && strings.Contains(node, parent) -} - -func dnIsUser(DN string) bool { - return len(DN) >= 4 && DN[0:4] == "uid=" -} diff --git a/prod.docker-compose.yaml b/prod.docker-compose.yaml index 3fcc5ac..7e02d04 100644 --- a/prod.docker-compose.yaml +++ b/prod.docker-compose.yaml @@ -5,9 +5,18 @@ services: dockerfile: Dockerfile context: . image: goldapps:stable - command: -dry -y -# volumes: -# - some_config.toml:/app/config.toml:ro -# - google_api_key1.json:/app/gapps1.json:ro -# - google_api_key2.json:/app/gapps2.json:ro -# - data_for_json_groups:/app/data \ No newline at end of file + restart: always + entrypoint: ./sleep_and_run.sh + command: [ + "-from ldap", + "-to gapps", + "-additions additions.json", + "-y", + "-dry" + ] + volumes: + - ./config.toml:/app/config.toml:ro + - ./gapps.json:/app/gapps.json:ro + - ./additions.json:/app/additions.json:ro + environment: + - WAIT=1h \ No newline at end of file diff --git a/services.go b/services.go deleted file mode 100644 index 5d0e264..0000000 --- a/services.go +++ /dev/null @@ -1,12 +0,0 @@ -package goldapps - -type GroupUpdateService interface { - DeleteGroup(group Group) error - UpdateGroup(groupUpdate GroupUpdate) error - AddGroup(group Group) error - GroupService -} - -type GroupService interface { - GetGroups() ([]Group, error) -} diff --git a/sleep_and_run.sh b/sleep_and_run.sh new file mode 100755 index 0000000..09161b6 --- /dev/null +++ b/sleep_and_run.sh @@ -0,0 +1,3 @@ +#!/bin/sh +echo Sleeping for "$WAIT" before running \"goldapps "$*"\" +sleep "$WAIT" && /app/goldapps $*