diff --git a/README.md b/README.md index fbab7dcc..1fc7d63e 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,16 @@ AWS SSO. To sync. regularly, you can run ssosync via AWS Lambda. You will find using the provided CDK deployment scripts the easiest method. Install the [AWS CDK](https://aws.amazon.com/cdk/) before you start. +## SAM + +You can use the AWS Serverless Application Model (SAM) to deploy this to your account. + +> Please, install the [AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html). + +Specify an Amazon S3 Bucket for the upload with `export S3_BUCKET=`. + +Execute `make package` in the console. Which will package and upload the function to the bucket. You can then use the `packaged.yaml` to configure and deploy the stack in [AWS CloudFormation Console](https://console.aws.amazon.com/cloudformation). + ### Using the right binary for AWS Lambda You require the AMD64 binary for AWS Lambda. This can be either downloaded from the diff --git a/cmd/lambda.go b/cmd/lambda.go deleted file mode 100644 index e675151c..00000000 --- a/cmd/lambda.go +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (c) 2020, Amazon.com, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package cmd - -import ( - "encoding/base64" - "fmt" - "io/ioutil" - "os" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/secretsmanager" - "github.com/awslabs/ssosync/internal" - "github.com/awslabs/ssosync/internal/config" -) - -// inLambda detects if we are running Lambda and will -// return true if we are -func inLambda() bool { - return len(os.Getenv("_LAMBDA_SERVER_PORT")) > 0 -} - -// writeSecretToFile will write the given secret out to a temporary file -// prefixed with 'name'. -func writeSecretToFile(secretKey string, name string) (string, error) { - s := session.Must(session.NewSession()) - svc := secretsmanager.New(s) - - r, err := svc.GetSecretValue(&secretsmanager.GetSecretValueInput{ - SecretId: aws.String(secretKey), - VersionStage: aws.String("AWSCURRENT"), - }) - - if err != nil { - return "", err - } - - var secretString string - - if r.SecretString != nil { - secretString = *r.SecretString - } else { - decodedBinarySecretBytes := make([]byte, base64.StdEncoding.DecodedLen(len(r.SecretBinary))) - l, err := base64.StdEncoding.Decode(decodedBinarySecretBytes, r.SecretBinary) - if err != nil { - return "", err - } - secretString = string(decodedBinarySecretBytes[:l]) - } - - f, err := ioutil.TempFile("", fmt.Sprintf("%s-*", name)) - - if err != nil { - return "", err - } - - if _, err = f.WriteString(secretString); err != nil { - return "", err - } - - if err := f.Close(); err != nil { - return "", err - } - - return f.Name(), nil -} - -// removeFileSilently handles the fact we want to delete temporary -// files - but we don't care if it fails. So we can use it in -// defer without warnings. -func removeFileSilently(name string) { - _ = os.Remove(name) -} - -// lambdaHandler is the Lambda entry point. -func lambdaHandler(cfg *config.Config) func() error { - return func() error { - if err := internal.DoSync(cfg); err != nil { - return err - } - - return nil - } -} diff --git a/cmd/root.go b/cmd/root.go index 21d69bb7..715c937e 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -16,12 +16,15 @@ package cmd import ( "fmt" + "os" "github.com/awslabs/ssosync/internal" "github.com/awslabs/ssosync/internal/config" - "github.com/pkg/errors" "github.com/aws/aws-lambda-go/lambda" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/secretsmanager" + "github.com/pkg/errors" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -57,8 +60,8 @@ Complete documentation is available at https://github.com/awslabs/ssosync`, // running inside of AWS Lambda, we use the Lambda // execution path. func Execute() { - if inLambda() { - lambda.Start(lambdaHandler(cfg)) + if cfg.IsLambda { + lambda.Start(rootCmd.Execute) } if err := rootCmd.Execute(); err != nil { @@ -69,6 +72,7 @@ func Execute() { func init() { // init config cfg = config.New() + cfg.IsLambda = len(os.Getenv("_LAMBDA_SERVER_PORT")) > 0 // initialize cobra cobra.OnInitialize(initConfig) @@ -87,12 +91,11 @@ func initConfig() { viper.SetEnvPrefix("ssosync") viper.AutomaticEnv() - viper.BindEnv("google_admin") - viper.BindEnv("google_credentials") - viper.BindEnv("scim_access_token") - viper.BindEnv("scim_endpoint") - viper.BindEnv("log_level") - viper.BindEnv("log_format") + for _, e := range []string{"google_admin", "google_credentials", "scim_access_token", "scim_endpoint", "log_level", "log_format"} { + if err := viper.BindEnv(e); err != nil { + log.Fatalf(errors.Wrap(err, "cannot bind environment variable").Error()) + } + } if err := viper.Unmarshal(&cfg); err != nil { log.Fatalf(errors.Wrap(err, "cannot unmarshal config").Error()) @@ -100,6 +103,40 @@ func initConfig() { // config logger logConfig(cfg) + + if cfg.IsLambda { + configLambda() + } +} + +func configLambda() { + s := session.Must(session.NewSession()) + svc := secretsmanager.New(s) + secrets := config.NewSecrets(svc) + + unwrap, err := secrets.GoogleAdminEmail() + if err != nil { + log.Fatalf(errors.Wrap(err, "cannot read config").Error()) + } + cfg.GoogleAdmin = unwrap + + unwrap, err = secrets.GoogleCredentials() + if err != nil { + log.Fatalf(errors.Wrap(err, "cannot read config").Error()) + } + cfg.GoogleCredentials = unwrap + + unwrap, err = secrets.SCIMAccessToken() + if err != nil { + log.Fatalf(errors.Wrap(err, "cannot read config").Error()) + } + cfg.SCIMAccessToken = unwrap + + unwrap, err = secrets.SCIMEndpointUrl() + if err != nil { + log.Fatalf(errors.Wrap(err, "cannot read config").Error()) + } + cfg.SCIMEndpoint = unwrap } func addFlags(cmd *cobra.Command, cfg *config.Config) { diff --git a/go.mod b/go.mod index 5bb81d0a..bca685c7 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.14 require ( github.com/BurntSushi/toml v0.3.1 github.com/aws/aws-lambda-go v1.17.0 - github.com/aws/aws-sdk-go v1.31.7 + github.com/aws/aws-sdk-go v1.33.7 github.com/golang/mock v1.4.3 github.com/golang/protobuf v1.4.1 // indirect github.com/pkg/errors v0.9.1 diff --git a/go.sum b/go.sum index 98b83ee5..9a708f85 100644 --- a/go.sum +++ b/go.sum @@ -32,8 +32,8 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/aws/aws-lambda-go v1.17.0 h1:Ogihmi8BnpmCNktKAGpNwSiILNNING1MiosnKUfU8m0= github.com/aws/aws-lambda-go v1.17.0/go.mod h1:FEwgPLE6+8wcGBTe5cJN3JWurd1Ztm9zN4jsXsjzKKw= -github.com/aws/aws-sdk-go v1.31.7 h1:TCA+pXKvzDMA3vVqhK21cCy5GarC8pTQb/DrVOWI3iY= -github.com/aws/aws-sdk-go v1.31.7/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= +github.com/aws/aws-sdk-go v1.33.7 h1:vOozL5hmWHHriRviVTQnUwz8l05RS0rehmEFymI+/x8= +github.com/aws/aws-sdk-go v1.33.7/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= diff --git a/internal/aws/client_test.go b/internal/aws/client_test.go index e117d28c..8114330f 100644 --- a/internal/aws/client_test.go +++ b/internal/aws/client_test.go @@ -57,7 +57,6 @@ func (r *httpReqMatcher) Matches(req interface{}) bool { if m.Body != nil { got, _ := ioutil.ReadAll(m.Body) if string(got) != r.body { - fmt.Println(string(got)) return false } } diff --git a/internal/config/config.go b/internal/config/config.go index fb3dcf31..4a3e9de8 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,6 +1,6 @@ package config -// Config contains a configuration for Autobot +// Config ... type Config struct { // Verbose toggles the verbosity Debug bool @@ -16,6 +16,8 @@ type Config struct { SCIMEndpoint string `mapstructure:"scim_endpoint"` // SCIMAccessToken ... SCIMAccessToken string `mapstructure:"scim_access_token"` + // IsLambda ... + IsLambda bool } const ( diff --git a/internal/config/secrets.go b/internal/config/secrets.go new file mode 100644 index 00000000..ccc2b6c2 --- /dev/null +++ b/internal/config/secrets.go @@ -0,0 +1,66 @@ +package config + +import ( + "encoding/base64" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/secretsmanager" +) + +// Secrets ... +type Secrets struct { + svc *secretsmanager.SecretsManager +} + +// NewSecrets ... +func NewSecrets(svc *secretsmanager.SecretsManager) *Secrets { + return &Secrets{ + svc: svc, + } +} + +// GoogleAdminEmail ... +func (s *Secrets) GoogleAdminEmail() (string, error) { + return s.getSecret("SSOSyncGoogleAdminEmail") +} + +// SCIMAccessToken ... +func (s *Secrets) SCIMAccessToken() (string, error) { + return s.getSecret("SSOSyncSCIMAccessToken") +} + +// SCIMEndpointUrl ... +func (s *Secrets) SCIMEndpointUrl() (string, error) { + return s.getSecret("SSOSyncSCIMEndpointUrl") +} + +// GoogleCredentials ... +func (s *Secrets) GoogleCredentials() (string, error) { + return s.getSecret("SSOSyncGoogleCredentials") +} + +func (s *Secrets) getSecret(secretKey string) (string, error) { + r, err := s.svc.GetSecretValue(&secretsmanager.GetSecretValueInput{ + SecretId: aws.String(secretKey), + VersionStage: aws.String("AWSCURRENT"), + }) + + if err != nil { + return "", err + } + + var secretString string + + if r.SecretString != nil { + secretString = *r.SecretString + } else { + decodedBinarySecretBytes := make([]byte, base64.StdEncoding.DecodedLen(len(r.SecretBinary))) + l, err := base64.StdEncoding.Decode(decodedBinarySecretBytes, r.SecretBinary) + if err != nil { + return "", err + } + secretString = string(decodedBinarySecretBytes[:l]) + } + + return secretString, nil +} diff --git a/internal/google/client.go b/internal/google/client.go index 133e5f54..0589af7a 100644 --- a/internal/google/client.go +++ b/internal/google/client.go @@ -16,7 +16,6 @@ package google import ( "context" - "fmt" "golang.org/x/oauth2/google" admin "google.golang.org/api/admin/directory/v1" @@ -70,8 +69,6 @@ func (c *client) GetUsers() (u []*admin.User, err error) { return nil }) - fmt.Println(err) - return u, err } diff --git a/internal/sync.go b/internal/sync.go index cc7f5fe1..55d5bd32 100644 --- a/internal/sync.go +++ b/internal/sync.go @@ -210,12 +210,17 @@ func (s *SyncGSuite) SyncGroups() error { func DoSync(cfg *config.Config) error { log.Info("Creating the Google and AWS Clients needed") - b, err := ioutil.ReadFile(cfg.GoogleCredentials) - if err != nil { - return err + creds := []byte(cfg.GoogleCredentials) + + if !cfg.IsLambda { + b, err := ioutil.ReadFile(cfg.GoogleCredentials) + if err != nil { + return err + } + creds = b } - googleClient, err := google.NewClient(cfg.GoogleAdmin, b) + googleClient, err := google.NewClient(cfg.GoogleAdmin, creds) if err != nil { return err } diff --git a/template.yaml b/template.yaml index 40cef9b9..31831a70 100644 --- a/template.yaml +++ b/template.yaml @@ -29,12 +29,15 @@ Parameters: GoogleCredentials: Type: String Description: Credentials to log into Google - GoogleToken: + GoogleAdminEmail: Type: String - Description: Token to use Google APIs - AWSToml: + Description: Google Admin email + SCIMEndpointUrl: Type: String - Description: TOML file from AWS SSO + Description: AWS SSO SCIM Endpoint Url + SCIMEndpointAccessToken: + Type: String + Description: AWS SSO SCIM AccessToken Resources: SSOSyncFunction: @@ -46,30 +49,46 @@ Resources: Variables: SSOSYNC_LOG_LEVEL: !Ref LogLevel SSOSYNC_LOG_FORMAT: !Ref LogFormat - SSOSYNC_GOOGLE_CREDENTIALS: !Ref GoogleCredentials - SSOSYNC_GOOGLE_TOKEN: !Ref GoogleToken - SSOSYNC_AWS_TOML: !Ref AWSToml - Events: - SyncScheduledEvent: - Type: Schedule - Properties: - Schedule: rate(30 minutes) + SSOSYNC_GOOGLE_CREDENTIALS: !Ref AWSGoogleCredentialsSecret + SSOSYNC_GOOGLE_ADMIN: !Ref AWSGoogleAdminEamil + SSOSYNC_SCIM_ENDPOINT: !Ref AWSSCIMEndpointSecret + SSOSYNC_SCIM_ACCESS_TOKEN: !Ref AWSSCIMAccessTokenSecret + Policies: + - Statement: + - Sid: SSMGetParameterPolicy + Effect: Allow + Action: + - "secretsmanager:Get*" + Resource: '*' + Events: + SyncScheduledEvent: + Type: Schedule + Name: AWSSyncSchedule + Properties: + Enabled: true + Schedule: 'rate(15 minutes)' - GoogleCredentialsSecret: + AWSGoogleCredentialsSecret: Type: 'AWS::SecretsManager::Secret' Properties: Name: SSOSyncGoogleCredentials SecretString: !Ref GoogleCredentials - GoogleTokenSecret: + AWSGoogleAdminEamil: + Type: 'AWS::SecretsManager::Secret' + Properties: + Name: SSOSyncGoogleAdminEmail + SecretString: !Ref GoogleAdminEmail + + AWSSCIMEndpointSecret: # This can be moved to custom provider Type: 'AWS::SecretsManager::Secret' Properties: - Name: SSOSyncGoogleToken - SecretString: !Ref GoogleToken + Name: SSOSyncSCIMEndpointUrl + SecretString: !Ref SCIMEndpointUrl - AWSTomlSecret: # This can be moved to custom provider + AWSSCIMAccessTokenSecret: # This can be moved to custom provider Type: 'AWS::SecretsManager::Secret' Properties: - Name: SSOSyncAWSToml - SecretString: !Ref AWSToml + Name: SSOSyncSCIMAccessToken + SecretString: !Ref SCIMEndpointAccessToken