Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,3 @@ archives:
format_overrides:
- goos: windows
format: zip

30 changes: 27 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,32 @@ as locally running the ssosync tool.

### Google

First, you have to setup your API. In the project you want to use go to the [Console](https://console.developers.google.com/apis) and select *API & Services* > *Enable APIs and Services*. Search for *Admin SDK* and *Enable* the API.
First, you have to setup your API. In the project you want to use go to the [Console](https://console.developers.google.com/apis) and select
*API & Services* > *Enable APIs and Services*. Search for *Admin SDK* and *Enable* the API.

You have to perform this [tutorial](https://developers.google.com/admin-sdk/directory/v1/guides/delegation) to create a service account that you use to sync your users. Save the `JSON file` you create during the process and rename it to `credentials.json`.
You have to perform this [tutorial](https://developers.google.com/admin-sdk/directory/v1/guides/delegation) to create a
service account that you use to sync your users. This is the service account that is used to impersonate a user
(via `--google-admin`). You have two possibilities to use this service account. Create a service account key credential,
as describe in the tutorial above. Or use
[Workload Identity Federation](https://cloud.google.com/iam/docs/workload-identity-federation).

#### Service account key credential

Save the `JSON file` you create during the process described in the tutorial above and rename it to `credentials.json`.
Please, keep this file safe, or store it in the AWS Secrets Manager.

#### Workload Identity Federation

Set up Workload Identity Federation for AWS as described
[here](https://cloud.google.com/iam/docs/workload-identity-federation-with-other-clouds) and save the `JSON file`, and
rename it to `credentials.json`. Provide the email address of service account used to impersonate a user using
`--google-service-account-email`.
Note that the `JSON file` created using this approach **does not** contain any sensitive data.

> You can also use the `--google-credentials` parameter to explicitly specify the file containing the credentials.
> Setting this parameter to an empty string will use
> [Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials).

> you can also use the `--google-credentials` parameter to explicitly specify the file with the service credentials. Please, keep this file safe, or store it in the AWS Secrets Manager

In the domain-wide delegation for the Admin API, you have to specify the following scopes for the user.

Expand All @@ -112,6 +133,9 @@ In the Search box type `Admin` and select the `Admin SDK` option. Click the `Ena

You will have to specify the email address of an admin via `--google-admin` to assume this users role in the Directory.

> When running this tool as AWS Lambda, the parameter `--google-credentials` is expected to contain the content of the
> `JSON file`.

### AWS

Go to the AWS Single Sign-On console in the region you have set up AWS SSO and select
Expand Down
1 change: 1 addition & 0 deletions SAR.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ There are general configuration parameters to the application stack.

* `GoogleCredentials` contains the content of the `credentials.json` file
* `GoogleAdminEmail` contains the email address of an admin
* `GoogleSAEmail` contains the email address of a Google service account used to impersonate the admin user

The secrets are stored in the [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/).

Expand Down
20 changes: 14 additions & 6 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import (
"github.com/aws/aws-lambda-go/lambda"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/codepipeline"
"github.com/aws/aws-sdk-go/service/codepipeline"
"github.com/aws/aws-sdk-go/service/secretsmanager"
"github.com/awslabs/ssosync/internal"
"github.com/awslabs/ssosync/internal/config"
Expand Down Expand Up @@ -69,7 +69,7 @@ Complete documentation is available at https://github.com/awslabs/ssosync`,
func Execute() {
if cfg.IsLambda {
log.Info("Executing as Lambda")
lambda.Start(Handler)
lambda.Start(Handler)
}

if err := rootCmd.Execute(); err != nil {
Expand All @@ -95,7 +95,7 @@ func Handler(ctx context.Context, event events.CodePipelineEvent) (string, error
jobID := event.CodePipelineJob.ID
if len(jobID) == 0 {
panic("CodePipeline Job ID is not set")
}
}
// mark the job as Failure.
cplFailure := &codepipeline.PutJobFailureResultInput{
JobId: aws.String(jobID),
Expand Down Expand Up @@ -124,10 +124,10 @@ func Handler(ctx context.Context, event events.CodePipelineEvent) (string, error
if cplErr != nil {
log.Fatalf(errors.Wrap(err, "Failed to update CodePipeline jobID status").Error())
}

return "Success", nil
}

if err != nil {
log.Fatalf(errors.Wrap(err, "Notifying Lambda and mark this execution as Failure").Error())
return "Failure", err
Expand Down Expand Up @@ -159,6 +159,7 @@ func initConfig() {

appEnvVars := []string{
"google_admin",
"google_sa_email",
"google_credentials",
"scim_access_token",
"scim_endpoint",
Expand Down Expand Up @@ -194,7 +195,7 @@ func initConfig() {
}

func configLambda() {
s := session.Must(session.NewSession())
s := session.Must(session.NewSession())
svc := secretsmanager.New(s)
secrets := config.NewSecrets(svc)

Expand All @@ -204,6 +205,12 @@ func configLambda() {
}
cfg.GoogleAdmin = unwrap

unwrap, err = secrets.GoogleSAEmail(os.Getenv("GOOGLE_SA_EMAIL"))
if err != nil {
log.Fatalf(errors.Wrap(err, "cannot read config").Error())
}
cfg.GoogleSAEmail = unwrap

unwrap, err = secrets.GoogleCredentials(os.Getenv("GOOGLE_CREDENTIALS"))
if err != nil {
log.Fatalf(errors.Wrap(err, "cannot read config: GOOGLE_CREDENTIALS").Error())
Expand Down Expand Up @@ -293,6 +300,7 @@ func addFlags(cmd *cobra.Command, cfg *config.Config) {
rootCmd.Flags().StringVarP(&cfg.SCIMEndpoint, "endpoint", "e", "", "AWS SSO SCIM API Endpoint")
rootCmd.Flags().StringVarP(&cfg.GoogleCredentials, "google-credentials", "c", config.DefaultGoogleCredentials, "path to Google Workspace credentials file")
rootCmd.Flags().StringVarP(&cfg.GoogleAdmin, "google-admin", "u", "", "Google Workspace admin user email")
rootCmd.Flags().StringVarP(&cfg.GoogleSAEmail, "google-service-account-email", "W", "", "Google Workload Identity Federation SA email. If set, google-credentials must be associated with a Workload Identity Federation json file")
rootCmd.Flags().StringSliceVar(&cfg.IgnoreUsers, "ignore-users", []string{}, "ignores these Google Workspace users")
rootCmd.Flags().StringSliceVar(&cfg.IgnoreGroups, "ignore-groups", []string{}, "ignores these Google Workspace groups")
rootCmd.Flags().StringSliceVar(&cfg.IncludeGroups, "include-groups", []string{}, "include only these Google Workspace groups, NOTE: only works when --sync-method 'users_groups'")
Expand Down
13 changes: 10 additions & 3 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,21 @@ package config
type Config struct {
// Verbose toggles the verbosity
Debug bool
// LogLevel is the level with with to log for this config
// LogLevel is the level that is used for logging
LogLevel string `mapstructure:"log_level"`
// LogFormat is the format that is used for logging
LogFormat string `mapstructure:"log_format"`
// GoogleCredentials ...
GoogleCredentials string `mapstructure:"google_credentials"`
// GoogleAdmin ...
GoogleAdmin string `mapstructure:"google_admin"`
GoogleAdmin string `mapstructure:"google_admin"`
// GoogleSAEmail is the email of a service account enabled for domain-wide delegation
// If nonempty, it is assumed that Workload Identity Federation is to be used. In that case, the
// specified service account needs to be configured for domain-wide delegation and the service account
// used for Workload Identity Federation must include "Service Account Token Creator" for the specified
// service account. Moreover, GoogleCredentials must be associated with a json file configured for Workload
// Identity Federation.
GoogleSAEmail string `mapstructure:"google_sa_email"`
// UserMatch ...
UserMatch string `mapstructure:"user_match"`
// GroupFilter ...
Expand All @@ -31,7 +38,7 @@ type Config struct {
IgnoreGroups []string `mapstructure:"ignore_groups"`
// Include groups ...
IncludeGroups []string `mapstructure:"include_groups"`
// SyncMethod allow to defined the sync method used to get the user and groups from Google Workspace
// SyncMethod allow to define the sync method used to get the user and groups from Google Workspace
SyncMethod string `mapstructure:"sync_method"`
// Region is the region that the identity store exists on
Region string `mapstructure:"region"`
Expand Down
8 changes: 8 additions & 0 deletions internal/config/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ func (s *Secrets) GoogleAdminEmail(secretArn string) (string, error) {
return s.getSecret(secretArn)
}

// GoogleSAEmail ...
func (s *Secrets) GoogleSAEmail(secretArn string) (string, error) {
if len([]rune(secretArn)) == 0 {
return s.getSecret("SSOSyncGoogleSAEmail")
}
return s.getSecret(secretArn)
}

// SCIMAccessToken ...
func (s *Secrets) SCIMAccessToken(secretArn string) (string, error) {
if len([]rune(secretArn)) == 0 {
Expand Down
55 changes: 54 additions & 1 deletion internal/google/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ import (
"errors"
"strings"

"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
admin "google.golang.org/api/admin/directory/v1"
"google.golang.org/api/impersonate"
"google.golang.org/api/option"
)

Expand All @@ -39,7 +41,28 @@ type client struct {
}

// NewClient creates a new client for Google's Admin API
func NewClient(ctx context.Context, adminEmail string, serviceAccountKey []byte) (Client, error) {
func NewClient(ctx context.Context, adminEmail, saEmail string, serviceAccountKey []byte) (Client, error) {
if saEmail == "" {
return newClientForServiceAccountKey(ctx, adminEmail, serviceAccountKey)
}

return newClientForWorkloadIdentityFederation(ctx, adminEmail, saEmail, serviceAccountKey)
}

func newClientForServiceAccountKey(ctx context.Context, adminEmail string, serviceAccountKey []byte) (Client, error) {
if serviceAccountKey == nil {
cred, err := google.FindDefaultCredentials(ctx, admin.AdminDirectoryGroupReadonlyScope,
admin.AdminDirectoryGroupMemberReadonlyScope,
admin.AdminDirectoryUserReadonlyScope)
if err != nil {
return nil, err
}
serviceAccountKey = cred.JSON
if serviceAccountKey == nil {
return nil, errors.New("default credentials not appropriate for JSON configuration")
}
}

config, err := google.JWTConfigFromJSON(serviceAccountKey, admin.AdminDirectoryGroupReadonlyScope,
admin.AdminDirectoryGroupMemberReadonlyScope,
admin.AdminDirectoryUserReadonlyScope)
Expand All @@ -63,6 +86,36 @@ func NewClient(ctx context.Context, adminEmail string, serviceAccountKey []byte)
}, nil
}

func newClientForWorkloadIdentityFederation(ctx context.Context, adminEmail, saEmail string, serviceAccountKey []byte) (Client, error) {
var err error
var ts oauth2.TokenSource
var config = impersonate.CredentialsConfig{
Subject: adminEmail,
Scopes: []string{admin.AdminDirectoryGroupReadonlyScope, admin.AdminDirectoryGroupMemberReadonlyScope, admin.AdminDirectoryUserReadonlyScope},
TargetPrincipal: saEmail,
}

if serviceAccountKey == nil {
ts, err = impersonate.CredentialsTokenSource(ctx, config)
} else {
ts, err = impersonate.CredentialsTokenSource(ctx, config, option.WithCredentialsJSON(serviceAccountKey))
}

if err != nil {
return nil, err
}
srv, err := admin.NewService(ctx, option.WithTokenSource(ts))

if err != nil {
return nil, err
}

return &client{
ctx: ctx,
service: srv,
}, nil
}

// GetDeletedUsers will get the deleted users from the Google's Admin API.
func (c *client) GetDeletedUsers() ([]*admin.User, error) {
u := make([]*admin.User, 0)
Expand Down
34 changes: 18 additions & 16 deletions internal/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ package internal
import (
"context"
"errors"
"io/ioutil"
"os"

"github.com/awslabs/ssosync/internal/aws"
"github.com/awslabs/ssosync/internal/config"
Expand Down Expand Up @@ -644,7 +644,7 @@ func getGroupOperations(awsGroups []*aws.Group, googleGroups []*admin.Group) (ad

// AWS Groups found and not found in google
for _, gGroup := range googleGroups {
if _, found := awsMap[gGroup.Name]; found {
if _, found := awsMap[gGroup.Name]; found {
log.WithField("gGroup", gGroup).Debug("equals")
equals = append(equals, awsMap[gGroup.Name])
} else {
Expand Down Expand Up @@ -746,16 +746,6 @@ func getGroupUsersOperations(gGroupsUsers map[string][]*admin.User, awsGroupsUse
func DoSync(ctx context.Context, cfg *config.Config) error {
log.Info("Syncing AWS users and groups from Google Workspace SAML Application")

creds := []byte(cfg.GoogleCredentials)

if !cfg.IsLambda {
b, err := ioutil.ReadFile(cfg.GoogleCredentials)
if err != nil {
return err
}
creds = b
}

// create a http client with retry and backoff capabilities
retryClient := retryablehttp.NewClient()

Expand All @@ -768,9 +758,21 @@ func DoSync(ctx context.Context, cfg *config.Config) error {

httpClient := retryClient.StandardClient()

googleClient, err := google.NewClient(ctx, cfg.GoogleAdmin, creds)
var creds []byte = nil
if cfg.GoogleCredentials != "" {
if cfg.IsLambda {
creds = []byte(cfg.GoogleCredentials)
} else {
b, err := os.ReadFile(cfg.GoogleCredentials)
if err != nil {
return err
}
creds = b
}
}
googleClient, err := google.NewClient(ctx, cfg.GoogleAdmin, cfg.GoogleSAEmail, creds)
if err != nil {
log.WithField("error", err).Warn("Problem establising a connection to Google directory")
log.WithField("error", err).Warn("Problem establising a connection to Google directory")
return err
}

Expand All @@ -781,7 +783,7 @@ func DoSync(ctx context.Context, cfg *config.Config) error {
Token: cfg.SCIMAccessToken,
})
if err != nil {
log.WithField("error", err).Warn("Problem establising a SCIM connection to AWS IAM Identity Center")
log.WithField("error", err).Warn("Problem establising a SCIM connection to AWS IAM Identity Center")
return err
}

Expand All @@ -792,7 +794,7 @@ func DoSync(ctx context.Context, cfg *config.Config) error {
})

if err != nil {
log.WithField("error", err).Warn("Problem establising a session for Identity Store")
log.WithField("error", err).Warn("Problem establising a session for Identity Store")
return err
}

Expand Down
Loading