diff --git a/README.md b/README.md index 4573850..6363bfd 100644 --- a/README.md +++ b/README.md @@ -139,14 +139,20 @@ to be updated. |------------|-----------------|---------------------|--------------| | id | externalIds | value | organization | | area | locations | area | desk | -| building | locations | buildingId | desk | -| costCenter | organizations | costCenter | (not set) | -| department | organizations | department | (not set) | -| title | organizations | title | (not set) | +| costCenter | organizations* | costCenter | (not set) | +| department | organizations* | department | (not set) | +| title | organizations* | title | (not set) | | phone | phones | value | work | | manager | relations | value | manager | | familyName | name | familyName | n/a | | givenName | name | givenName | n/a | + +Custom schema properties can be added using dot notation. For example, a +custom property with Field name `Building` in the custom schema `Location` +is represented as `Location.Building`. + +__\* CAUTION:__ updating any field in `organizations` will overwrite all +existing organizations Following is an example configuration listing all available fields: @@ -204,7 +210,7 @@ Following is an example configuration listing all available fields: }, { "Source": "building", - "Destination": "building", + "Destination": "Location.Building", "required": false }, { @@ -318,3 +324,25 @@ as the `DelegatedAdminEmail` value under `Destination`/`ExtraJSON`. ``` `ListClientsPageLimit` and `BatchSizePerMinute` are optional. Their defaults are as shown in the example config. + +### Exporting logs from CloudWatch + +The log messages in CloudWatch can be viewed on the AWS Management Console. If +an exported text or json file is needed, the AWS CLI tool can be used as +follows: + +```shell script +aws configure +aws logs get-log-events \ + --log-group-name "/aws/lambda/lambda-name" \ + --log-stream-name '2019/11/14/[$LATEST]0123456789abcdef0123456789abcdef' \ + --output text \ + --query 'events[*].message' +``` + +Replace `/aws/lambda/lambda-name` with the actual log group name and +`2019/11/14/[$LATEST]0123456789abcdef0123456789abcdef` with the actual log +stream. Note the single quotes around the log stream name to prevent the shell +from interpreting the `$` character. `--output text` can be changed to +`--output json` if desired. Timestamps are available if needed, but omitted +in this example by the `--query` string. diff --git a/googledest/users.go b/googledest/users.go index 3fac5d3..f478441 100644 --- a/googledest/users.go +++ b/googledest/users.go @@ -8,10 +8,10 @@ import ( "sync" "sync/atomic" - admin "google.golang.org/api/admin/directory/v1" - personnel_sync "github.com/silinternational/personnel-sync" "golang.org/x/net/context" + admin "google.golang.org/api/admin/directory/v1" + "google.golang.org/api/googleapi" ) type GoogleUsersConfig struct { @@ -74,7 +74,6 @@ func extractData(user admin.User) personnel_sync.Person { if found := findFirstMatchingType(user.Locations, "desk"); found != nil { setStringFromInterface(found["area"], newPerson.Attributes, "area") - setStringFromInterface(found["buildingId"], newPerson.Attributes, "building") } if found := findFirstMatchingType(user.Organizations, ""); found != nil { @@ -96,6 +95,14 @@ func extractData(user admin.User) personnel_sync.Person { newPerson.Attributes["givenName"] = user.Name.GivenName } + for schemaKey, schemaVal := range user.CustomSchemas { + var schema map[string]string + _ = json.Unmarshal(schemaVal, &schema) + for propertyKey, propertyVal := range schema { + newPerson.Attributes[schemaKey+"."+propertyKey] = propertyVal + } + } + return newPerson } @@ -139,7 +146,8 @@ func setStringFromInterface(i interface{}, m map[string]string, key string) { func (g *GoogleUsers) ListUsers() ([]personnel_sync.Person, error) { var usersList []*admin.User usersListCall := g.AdminService.Users.List() - usersListCall.Customer("my_customer") + usersListCall.Customer("my_customer") // query all domains in this GSuite + usersListCall.Projection("full") // include custom fields err := usersListCall.Pages(context.TODO(), func(users *admin.Users) error { usersList = append(usersList, users.Users...) return nil @@ -181,44 +189,81 @@ func (g *GoogleUsers) ApplyChangeSet( func newUserForUpdate(person personnel_sync.Person, oldUser admin.User) (admin.User, error) { user := admin.User{} var err error - - newName := admin.UserName{ - GivenName: person.Attributes["givenName"], - FamilyName: person.Attributes["familyName"], - } - user.Name = &newName - - if person.Attributes["id"] != "" { - user.ExternalIds, err = updateIDs(person.Attributes["id"], oldUser.ExternalIds) - if err != nil { - return admin.User{}, err + var organization admin.UserOrganization + isOrgModified := false + + for key, val := range person.Attributes { + switch key { + case "givenName": + if user.Name == nil { + user.Name = &admin.UserName{GivenName: val} + } else { + user.Name.GivenName = val + } + + case "familyName": + if user.Name == nil { + user.Name = &admin.UserName{FamilyName: val} + } else { + user.Name.FamilyName = val + } + + case "id": + user.ExternalIds, err = updateIDs(val, oldUser.ExternalIds) + if err != nil { + return admin.User{}, err + } + + case "area": + user.Locations, err = updateLocations(val, oldUser.Locations) + if err != nil { + return admin.User{}, err + } + + case "costCenter": + organization.CostCenter = val + isOrgModified = true + + case "department": + organization.Department = val + isOrgModified = true + + case "title": + organization.Title = val + isOrgModified = true + + case "phone": + user.Phones, err = updatePhones(val, oldUser.Phones) + if err != nil { + return admin.User{}, err + } + + case "manager": + user.Relations, err = updateRelations(val, oldUser.Relations) + if err != nil { + return admin.User{}, err + } + + default: + keys := strings.SplitN(key, ".", 2) + if len(keys) < 2 { + continue + } + + j, err := json.Marshal(&map[string]string{keys[1]: val}) + if err != nil { + return admin.User{}, fmt.Errorf("error marshaling custom schema, %s", err) + } + + user.CustomSchemas = map[string]googleapi.RawMessage{ + keys[0]: j, + } } } - user.Locations, err = updateLocations(person.Attributes["area"], person.Attributes["building"], oldUser.Locations) - if err != nil { - return admin.User{}, err - } - - // NOTICE: this will overwrite any and all existing Organizations - user.Organizations = []admin.UserOrganization{{ - CostCenter: person.Attributes["costCenter"], - Department: person.Attributes["department"], - Title: person.Attributes["title"], - }} - - if person.Attributes["phone"] != "" { - user.Phones, err = updatePhones(person.Attributes["phone"], oldUser.Phones) - if err != nil { - return admin.User{}, err - } - } - - if person.Attributes["manager"] != "" { - user.Relations, err = updateRelations(person.Attributes["manager"], oldUser.Relations) - if err != nil { - return admin.User{}, err - } + if isOrgModified { + // NOTICE: this will overwrite any and all existing Organizations + user.Organizations = []admin.UserOrganization{organization} } return user, nil @@ -317,11 +362,10 @@ func updateIDs(newID string, oldIDs interface{}) ([]admin.UserExternalId, error) return IDs, nil } -func updateLocations(newArea, newBuilding string, oldLocations interface{}) ([]admin.UserLocation, error) { +func updateLocations(newArea string, oldLocations interface{}) ([]admin.UserLocation, error) { locations := []admin.UserLocation{{ - Type: "desk", - Area: newArea, - BuildingId: newBuilding, + Type: "desk", + Area: newArea, }} if oldLocations == nil { diff --git a/googledest/users_test.go b/googledest/users_test.go index eb98692..840ccc1 100644 --- a/googledest/users_test.go +++ b/googledest/users_test.go @@ -6,6 +6,8 @@ import ( "strconv" "testing" + "google.golang.org/api/googleapi" + personnel_sync "github.com/silinternational/personnel-sync" admin "google.golang.org/api/admin/directory/v1" ) @@ -180,9 +182,8 @@ func TestGoogleUsers_extractData(t *testing.T) { "value": "12345", }}, Locations: []interface{}{map[string]interface{}{ - "area": "An area", - "buildingId": "A building", - "type": "desk", + "area": "An area", + "type": "desk", }}, Name: &admin.UserName{ FamilyName: "Jones", @@ -203,21 +204,24 @@ func TestGoogleUsers_extractData(t *testing.T) { "type": "manager", "value": "manager@example.com", }}, + CustomSchemas: map[string]googleapi.RawMessage{ + "Location": []byte(`{"Building":"A building"}`), + }, }, want: personnel_sync.Person{ CompareValue: "email@example.com", Attributes: map[string]string{ - "email": "email@example.com", - "familyName": "Jones", - "givenName": "John", - "id": "12345", - "area": "An area", - "building": "A building", - "costCenter": "A cost center", - "department": "A department", - "title": "A title", - "phone": "555-1212", - "manager": "manager@example.com", + "email": "email@example.com", + "familyName": "Jones", + "givenName": "John", + "id": "12345", + "area": "An area", + "costCenter": "A cost center", + "department": "A department", + "title": "A title", + "phone": "555-1212", + "manager": "manager@example.com", + "Location.Building": "A building", }, }, }, @@ -273,23 +277,20 @@ func TestGoogleUsers_extractData(t *testing.T) { PrimaryEmail: "email@example.com", Locations: []interface{}{ map[string]interface{}{ - "area": "Custom area", - "buildingId": "Custom building", - "type": "custom", + "area": "Custom area", + "type": "custom", }, map[string]interface{}{ - "area": "An area", - "buildingId": "A building", - "type": "desk", + "area": "An area", + "type": "desk", }, }, }, want: personnel_sync.Person{ CompareValue: "email@example.com", Attributes: map[string]string{ - "email": "email@example.com", - "area": "An area", - "building": "A building", + "email": "email@example.com", + "area": "An area", }, }, }, @@ -301,9 +302,8 @@ func TestGoogleUsers_extractData(t *testing.T) { "value": 12345, }}, Locations: []interface{}{map[string]interface{}{ - "type": "desk", - "area": 1.0, - "buildingId": 1, + "type": "desk", + "area": 1.0, }}, Organizations: []interface{}{map[string]interface{}{ "costCenter": []string{"A cost center"}, @@ -348,17 +348,17 @@ func Test_newUserForUpdate(t *testing.T) { person: personnel_sync.Person{ CompareValue: "email@example.com", Attributes: map[string]string{ - "email": "email@example.com", - "familyName": "Jones", - "givenName": "John", - "id": "12345", - "area": "An area", - "building": "A building", - "costCenter": "A cost center", - "department": "A department", - "title": "A title", - "phone": "555-1212", - "manager": "manager@example.com", + "email": "email@example.com", + "familyName": "Jones", + "givenName": "John", + "id": "12345", + "area": "An area", + "costCenter": "A cost center", + "department": "A department", + "title": "A title", + "phone": "555-1212", + "manager": "manager@example.com", + "Location.Building": "A building", }, }, want: admin.User{ @@ -367,9 +367,8 @@ func Test_newUserForUpdate(t *testing.T) { Value: "12345", }}, Locations: []admin.UserLocation{{ - Area: "An area", - BuildingId: "A building", - Type: "desk", + Area: "An area", + Type: "desk", }}, Name: &admin.UserName{ FamilyName: "Jones", @@ -388,6 +387,9 @@ func Test_newUserForUpdate(t *testing.T) { Type: "manager", Value: "manager@example.com", }}, + CustomSchemas: map[string]googleapi.RawMessage{ + "Location": []byte(`{"Building":"A building"}`), + }, }, }, } @@ -489,19 +491,16 @@ func Test_updateLocations(t *testing.T) { tests := []struct { name string newArea string - newBuilding string oldLocations interface{} want []admin.UserLocation }{ { - name: "desk and custom", - newArea: "Area 2", - newBuilding: "Bldg 2", + name: "desk and custom", + newArea: "Area 2", oldLocations: []interface{}{ map[string]interface{}{ - "type": "desk", - "area": "Area 1", - "buildingId": "Bldg 1", + "type": "desk", + "area": "Area 1", }, map[string]interface{}{ "type": "custom", @@ -515,9 +514,8 @@ func Test_updateLocations(t *testing.T) { }, want: []admin.UserLocation{ { - Type: "desk", - Area: "Area 2", - BuildingId: "Bldg 2", + Type: "desk", + Area: "Area 2", }, { Type: "custom", @@ -531,28 +529,24 @@ func Test_updateLocations(t *testing.T) { }, }, { - name: "desk only", - newArea: "Area 2", - newBuilding: "Bldg 2", + name: "desk only", + newArea: "Area 2", oldLocations: []interface{}{ map[string]interface{}{ - "type": "desk", - "area": "Area 1", - "buildingId": "Bldg 1", + "type": "desk", + "area": "Area 1", }, }, want: []admin.UserLocation{ { - Type: "desk", - Area: "Area 2", - BuildingId: "Bldg 2", + Type: "desk", + Area: "Area 2", }, }, }, { - name: "custom only", - newArea: "Area 2", - newBuilding: "Bldg 2", + name: "custom only", + newArea: "Area 2", oldLocations: []interface{}{ map[string]interface{}{ "type": "custom", @@ -566,9 +560,8 @@ func Test_updateLocations(t *testing.T) { }, want: []admin.UserLocation{ { - Type: "desk", - Area: "Area 2", - BuildingId: "Bldg 2", + Type: "desk", + Area: "Area 2", }, { Type: "custom", @@ -584,7 +577,7 @@ func Test_updateLocations(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got, err := updateLocations(tt.newArea, tt.newBuilding, tt.oldLocations); err != nil { + if got, err := updateLocations(tt.newArea, tt.oldLocations); err != nil { t.Errorf("updateLocations() error: %s", err) } else if !reflect.DeepEqual(got, tt.want) { t.Errorf("updateLocations():\n%+v\nwant:\n%+v", got, tt.want)