diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 00000000..2c1bbf55 --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,311 @@ +# This file was downloaded from https://gist.github.com/maratori/47a4d00457a92aa426dbd48a18776322 +# This code is licensed under the terms of the MIT license. + +## Golden config for golangci-lint v1.52.2 +# +# This is the best config for golangci-lint based on my experience and opinion. +# It is very strict, but not extremely strict. +# Feel free to adopt and change it for your needs. + +run: + # Timeout for analysis, e.g. 30s, 5m. + # Default: 1m + timeout: 3m + + +# This file contains only configs which differ from defaults. +# All possible options can be found here https://github.com/golangci/golangci-lint/blob/master/.golangci.reference.yml +linters-settings: + cyclop: + # The maximal code complexity to report. + # Default: 10 + max-complexity: 30 + # The maximal average package complexity. + # If it's higher than 0.0 (float) the check is enabled + # Default: 0.0 + package-average: 10.0 + + errcheck: + # Report about not checking of errors in type assertions: `a := b.(MyStruct)`. + # Such cases aren't reported by default. + # Default: false + check-type-assertions: true + + exhaustive: + # Program elements to check for exhaustiveness. + # Default: [ switch ] + check: + - switch + - map + + exhaustruct: + # List of regular expressions to exclude struct packages and names from check. + # Default: [] + exclude: + # std libs + - "^net/http.Client$" + - "^net/http.Cookie$" + - "^net/http.Request$" + - "^net/http.Response$" + - "^net/http.Server$" + - "^net/http.Transport$" + - "^net/url.URL$" + - "^os/exec.Cmd$" + - "^reflect.StructField$" + # public libs + - "^github.com/Shopify/sarama.Config$" + - "^github.com/Shopify/sarama.ProducerMessage$" + - "^github.com/mitchellh/mapstructure.DecoderConfig$" + - "^github.com/prometheus/client_golang/.+Opts$" + - "^github.com/spf13/cobra.Command$" + - "^github.com/spf13/cobra.CompletionOptions$" + - "^github.com/stretchr/testify/mock.Mock$" + - "^github.com/testcontainers/testcontainers-go.+Request$" + - "^github.com/testcontainers/testcontainers-go.FromDockerfile$" + - "^golang.org/x/tools/go/analysis.Analyzer$" + - "^google.golang.org/protobuf/.+Options$" + - "^gopkg.in/yaml.v3.Node$" + + funlen: + # Checks the number of lines in a function. + # If lower than 0, disable the check. + # Default: 60 + lines: 100 + # Checks the number of statements in a function. + # If lower than 0, disable the check. + # Default: 40 + statements: 50 + + gocognit: + # Minimal code complexity to report. + # Default: 30 (but we recommend 10-20) + min-complexity: 20 + + gocritic: + # Settings passed to gocritic. + # The settings key is the name of a supported gocritic checker. + # The list of supported checkers can be find in https://go-critic.github.io/overview. + settings: + captLocal: + # Whether to restrict checker to params only. + # Default: true + paramsOnly: false + underef: + # Whether to skip (*x).method() calls where x is a pointer receiver. + # Default: true + skipRecvDeref: false + + gomnd: + # List of function patterns to exclude from analysis. + # Values always ignored: `time.Date`, + # `strconv.FormatInt`, `strconv.FormatUint`, `strconv.FormatFloat`, + # `strconv.ParseInt`, `strconv.ParseUint`, `strconv.ParseFloat`. + # Default: [] + ignored-functions: + - os.Chmod + - os.Mkdir + - os.MkdirAll + - os.OpenFile + - os.WriteFile + - prometheus.ExponentialBuckets + - prometheus.ExponentialBucketsRange + - prometheus.LinearBuckets + + gomodguard: + blocked: + # List of blocked modules. + # Default: [] + modules: + - github.com/golang/protobuf: + recommendations: + - google.golang.org/protobuf + reason: "see https://developers.google.com/protocol-buffers/docs/reference/go/faq#modules" + - github.com/satori/go.uuid: + recommendations: + - github.com/google/uuid + reason: "satori's package is not maintained" + - github.com/gofrs/uuid: + recommendations: + - github.com/google/uuid + reason: "gofrs' package is not go module" + + govet: + # Enable all analyzers. + # Default: false + enable-all: true + # Disable analyzers by name. + # Run `go tool vet help` to see all analyzers. + # Default: [] + disable: + - fieldalignment # too strict + # Settings per analyzer. + settings: + shadow: + # Whether to be strict about shadowing; can be noisy. + # Default: false + strict: true + + nakedret: + # Make an issue if func has more lines of code than this setting, and it has naked returns. + # Default: 30 + max-func-lines: 0 + + nolintlint: + # Exclude following linters from requiring an explanation. + # Default: [] + allow-no-explanation: [ funlen, gocognit, lll ] + # Enable to require an explanation of nonzero length after each nolint directive. + # Default: false + require-explanation: true + # Enable to require nolint directives to mention the specific linter being suppressed. + # Default: false + require-specific: true + + rowserrcheck: + # database/sql is always checked + # Default: [] + packages: + - github.com/jmoiron/sqlx + + tenv: + # The option `all` will run against whole test files (`_test.go`) regardless of method/function signatures. + # Otherwise, only methods that take `*testing.T`, `*testing.B`, and `testing.TB` as arguments are checked. + # Default: false + all: true + + +linters: + disable-all: true + enable: + ## enabled by default + - errcheck # checking for unchecked errors, these unchecked errors can be critical bugs in some cases + - gosimple # specializes in simplifying a code + - govet # reports suspicious constructs, such as Printf calls whose arguments do not align with the format string + - ineffassign # detects when assignments to existing variables are not used + - staticcheck # is a go vet on steroids, applying a ton of static analysis checks + - typecheck # like the front-end of a Go compiler, parses and type-checks Go code + - unused # checks for unused constants, variables, functions and types + ## disabled by default + - asasalint # checks for pass []any as any in variadic func(...any) + - asciicheck # checks that your code does not contain non-ASCII identifiers + - bidichk # checks for dangerous unicode character sequences + - bodyclose # checks whether HTTP response body is closed successfully + - cyclop # checks function and package cyclomatic complexity + - dupl # tool for code clone detection + - durationcheck # checks for two durations multiplied together + - errname # checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error + - errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13 + - execinquery # checks query string in Query function which reads your Go src files and warning it finds + - exhaustive # checks exhaustiveness of enum switch statements + - exportloopref # checks for pointers to enclosing loop variables + - forbidigo # forbids identifiers + - funlen # tool for detection of long functions + - gocheckcompilerdirectives # validates go compiler directive comments (//go:) + - gochecknoglobals # checks that no global variables exist + - gochecknoinits # checks that no init functions are present in Go code + - gocognit # computes and checks the cognitive complexity of functions + - goconst # finds repeated strings that could be replaced by a constant + - gocritic # provides diagnostics that check for bugs, performance and style issues + - gocyclo # computes and checks the cyclomatic complexity of functions + - godot # checks if comments end in a period + - goimports # in addition to fixing imports, goimports also formats your code in the same style as gofmt + - gomnd # detects magic numbers + - gomoddirectives # manages the use of 'replace', 'retract', and 'excludes' directives in go.mod + - gomodguard # allow and block lists linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations + - goprintffuncname # checks that printf-like functions are named with f at the end + - gosec # inspects source code for security problems + - lll # reports long lines + - loggercheck # checks key value pairs for common logger libraries (kitlog,klog,logr,zap) + - makezero # finds slice declarations with non-zero initial length + - nakedret # finds naked returns in functions greater than a specified function length + - nestif # reports deeply nested if statements + - nilerr # finds the code that returns nil even if it checks that the error is not nil + - nilnil # checks that there is no simultaneous return of nil error and an invalid value + - noctx # finds sending http request without context.Context + - nolintlint # reports ill-formed or insufficient nolint directives + - nosprintfhostport # checks for misuse of Sprintf to construct a host with port in a URL + - predeclared # finds code that shadows one of Go's predeclared identifiers + - promlinter # checks Prometheus metrics naming via promlint + - reassign # checks that package variables are not reassigned + - revive # fast, configurable, extensible, flexible, and beautiful linter for Go, drop-in replacement of golint + - rowserrcheck # checks whether Err of rows is checked successfully + - sqlclosecheck # checks that sql.Rows and sql.Stmt are closed + - stylecheck # is a replacement for golint + - tenv # detects using os.Setenv instead of t.Setenv since Go1.17 + - testableexamples # checks if examples are testable (have an expected output) + - testpackage # makes you use a separate _test package + - tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes + - unconvert # removes unnecessary type conversions + - unparam # reports unused function parameters + - usestdlibvars # detects the possibility to use variables/constants from the Go standard library + - wastedassign # finds wasted assignment statements + - whitespace # detects leading and trailing whitespace + - godox # detects FIXME, TODO and other comment keywords + + ## you may want to enable + #- musttag # enforces field tags in (un)marshaled structs + #- decorder # checks declaration order and count of types, constants, variables and functions + #- exhaustruct # [highly recommend to enable] checks if all structure fields are initialized + #- gci # controls golang package import order and makes it always deterministic + #- ginkgolinter # [if you use ginkgo/gomega] enforces standards of using ginkgo and gomega + #- goheader # checks is file header matches to pattern + #- interfacebloat # checks the number of methods inside an interface + #- ireturn # accept interfaces, return concrete types + #- prealloc # [premature optimization, but can be used in some cases] finds slice declarations that could potentially be preallocated + #- varnamelen # [great idea, but too many false positives] checks that the length of a variable's name matches its scope + #- wrapcheck # checks that errors returned from external packages are wrapped + #- nonamedreturns # reports all named returns + ## disabled + #- containedctx # detects struct contained context.Context field + #- contextcheck # [too many false positives] checks the function whether use a non-inherited context + #- depguard # [replaced by gomodguard] checks if package imports are in a list of acceptable packages + #- dogsled # checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) + #- dupword # [useless without config] checks for duplicate words in the source code + #- errchkjson # [don't see profit + I'm against of omitting errors like in the first example https://github.com/breml/errchkjson] checks types passed to the json encoding functions. Reports unsupported types and optionally reports occasions, where the check for the returned error can be omitted + #- forcetypeassert # [replaced by errcheck] finds forced type assertions + #- goerr113 # [too strict] checks the errors handling expressions + #- gofmt # [replaced by goimports] checks whether code was gofmt-ed + #- gofumpt # [replaced by goimports, gofumports is not available yet] checks whether code was gofumpt-ed + #- grouper # analyzes expression groups + #- importas # enforces consistent import aliases + #- maintidx # measures the maintainability index of each function + #- misspell # [useless] finds commonly misspelled English words in comments + #- nlreturn # [too strict and mostly code is not more readable] checks for a new line before return and branch statements to increase code clarity + #- paralleltest # [too many false positives] detects missing usage of t.Parallel() method in your Go test + #- tagliatelle # checks the struct tags + #- thelper # detects golang test helpers without t.Helper() call and checks the consistency of test helpers + #- wsl # [too strict and mostly code is not more readable] whitespace linter forces you to use empty lines + + ## deprecated + #- deadcode # [deprecated, replaced by unused] finds unused code + #- exhaustivestruct # [deprecated, replaced by exhaustruct] checks if all struct's fields are initialized + #- golint # [deprecated, replaced by revive] golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes + #- ifshort # [deprecated] checks that your code uses short syntax for if-statements whenever possible + #- interfacer # [deprecated] suggests narrower interface types + #- maligned # [deprecated, replaced by govet fieldalignment] detects Go structs that would take less memory if their fields were sorted + #- nosnakecase # [deprecated, replaced by revive var-naming] detects snake case of variable naming and function name + #- scopelint # [deprecated, replaced by exportloopref] checks for unpinned variables in go programs + #- structcheck # [deprecated, replaced by unused] finds unused struct fields + #- varcheck # [deprecated, replaced by unused] finds unused global variables and constants + + +issues: + # Maximum count of issues with the same text. + # Set to 0 to disable. + # Default: 3 + max-same-issues: 50 + + exclude-rules: + - source: "(noinspection|TODO)" + linters: [ godot ] + - source: "//noinspection" + linters: [ gocritic ] + - path: "_test\\.go" + linters: + - bodyclose + - dupl + - funlen + - goconst + - gosec + - noctx + - wrapcheck \ No newline at end of file diff --git a/README.md b/README.md index 76674693..7b5b1334 100644 --- a/README.md +++ b/README.md @@ -30,12 +30,12 @@ Use the `login` command to retrieve an unscoped token either by logging in direc ### Service Provider Login (IAM) To log in directly with the Open Telekom Cloud's IAM, you will have to supply the domain name you're attempting to log in to (usually starting with "OTC-EU", following the region and a longer identifier), your username and password. -`otc-auth login iam --os-username --os-password --os-domain-name ` +`otc-auth login iam --os-username --os-password --os-domain-name --region ` In addition, it is possible to use MFA if that's desired and/or required. In this case both arguments `--os-user-domain-id` and `--totp` are required. The user id can be obtained in the "My Credentials" page on the OTC. ``` -otc-auth login iam --os-username --os-password --os-domain-name --os-user-domain-id --totp <6_digit_token> +otc-auth login iam --os-username --os-password --os-domain-name --os-user-domain-id --totp <6_digit_token> --region ``` The OTP Token is 6-digit long and refreshes every 30 seconds. For more information on MFA please refer to the [OTC's documentation](https://docs.otc.t-systems.com/en-us/usermanual/iam/iam_10_0002.html). @@ -46,14 +46,14 @@ You can log in with an external IdP using either the `saml` or the `oidc` protoc #### External IdP and SAML The SAML login flow is SP initiated and requires you to send username and password to the SP. The SP then authorizes you with the configured IdP and returns either an unscoped token or an error, if the user is not allowed to log in. -```otc-auth login idp-saml --os-username --os-password --idp-name --idp-url --os-domain-name ``` +```otc-auth login idp-saml --os-username --os-password --idp-name --idp-url --os-domain-name --region ``` At the moment, no MFA is supported for this login flow. #### External IdP and OIDC The OIDC login flow is user initiated and will open a browser window with the IdP's authorization URL for the user to log in as desired. This flow does support MFA (this requires it to be configured on the IdP). After being successfully authenticated with the IdP, the SP will be contacted with the corresponding credentials and will return either an unscoped token or an error, if the user is not allowed to log in. -```otc-auth login idp-oidc --idp-name --idp-url --client-id --os-domain-name [--client-secret ]``` +```otc-auth login idp-oidc --idp-name --idp-url --client-id --os-domain-name --region [--client-secret ]``` The argument `--client-id` is required, but the argument `--client-secret` is only needed if configured on the IdP. @@ -67,6 +67,7 @@ otc-auth login idp-oidc \ --idp-name NameOfClientInIdp \ --idp-url IdpAuthUrl \ --os-domain-name YourDomainName \ + --region YourRegion \ --client-id NameOfIdpInOtcIam \ --client-secret ClientSecretForTheClientInIdp \ --service-account @@ -84,7 +85,7 @@ The default value is `openid,profile,roles,name,groups,email` ### Remove Login Clouds are differentiated by their identifier `--os-domain-name`. To delete a cloud, use the `remove` command. -`otc-auth login remove --os-domain-name ` +`otc-auth login remove --os-domain-name --region ` ## List Projects It is possible to get a list of all projects in the current cloud. For that, use the following command. @@ -97,18 +98,18 @@ Use the `cce` command to retrieve a list of available clusters in your project a To retrieve a list of clusters for a project use the following command. The project name will be checked against the ones in the cloud at the moment of the request. If the desired project isn't found, you will receive an error message. -`otc-auth cce list --os-domain-name --os-project-name ` +`otc-auth cce list --os-domain-name --region --os-project-name ` To retrieve the remote kube configuration file (and merge to your local one) use the following command: -`otc-auth cce get-kube-config --os-domain-name --os-project-name --cluster ` +`otc-auth cce get-kube-config --os-domain-name --region --os-project-name --cluster ` Alternatively you can pass the argument `--days-valid` to set the period of days the configuration will be valid, the default is 7 days. ## Manage Access Key and Secret Key Pair You can use the OTC-Auth tool to download the AK/SK pair directly from the OTC. It will download the "ak-sk-env.sh" file to the current directory. The file contains four environment variables. -`otc-auth access-token create --os-domain-name ` +`otc-auth access-token create --os-domain-name --region ` The "ak-sk-env.sh" file must then be sourced before you can start using the environment variables. @@ -118,7 +119,7 @@ reuse the clouds.yaml with terraform. If you execute this command -`otc-auth openstack config-create` +`otc-auth openstack config-create --region ` It will create a cloud config for every project which you have access to and generate a scoped token. After that it overrides the the clouds.yaml (by default: ~/.config/openstack/clouds.yaml) file. @@ -132,6 +133,7 @@ The OTC-Auth tool also provides environment variables for all the required argum | CLIENT_SECRET | `--client-secret` | `s` | Client secret as configured on the IdP | | CLUSTER_NAME | `--cluster` | `c` | Cluster name on the OTC | | OS_DOMAIN_NAME | `--os-domain-name` | `d` | Domain Name from OTC Tenant | +| REGION | `--region` | `r` | Region code for the cloud (eu-de for example) | | OS_PASSWORD | `--os-password` | `p` | Password (iam or idp) | | OS_PROJECT_NAME | `--os-project-name` | `p` | Project name on the OTC | | OS_USER_DOMAIN_ID | `--os-user-domain-id` | `i` | User id from OTC Tenant | diff --git a/accesstoken/accesstoken.go b/accesstoken/accesstoken.go index 9ac7a69b..446e3dfa 100644 --- a/accesstoken/accesstoken.go +++ b/accesstoken/accesstoken.go @@ -1,49 +1,137 @@ package accesstoken import ( + "errors" "fmt" - "github.com/go-http-utils/headers" - "net/http" + "log" + "strings" + "otc-auth/common" "otc-auth/common/endpoints" - "otc-auth/common/headervalues" - "otc-auth/common/xheaders" "otc-auth/config" - "strconv" - "strings" -) -func CreateAccessToken(durationSeconds int) { - println("Creating access token file...") - - response := getAccessTokenFromServiceProvider(strconv.Itoa(durationSeconds)) - bodyBytes := common.GetBodyBytesFromResponse(response) + golangsdk "github.com/opentelekomcloud/gophertelekomcloud" + "github.com/opentelekomcloud/gophertelekomcloud/openstack" + "github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/credentials" + "github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/tokens" +) - accessTokenCreationResponse := common.DeserializeJsonForType[TokenCreationResponse](bodyBytes) +func CreateAccessToken(tokenDescription string) { + log.Println("Creating access token file with GTC...") + resp, err := getAccessTokenFromServiceProvider(tokenDescription) + if err != nil { + // Handle error currently thrown when logged in by OIDC + var convErr golangsdk.ErrDefault404 + if errors.As(err, &convErr) { + if convErr.ErrUnexpectedResponseCode.Actual == 404 && + strings.Contains(convErr.ErrUnexpectedResponseCode.URL, + "OS-CREDENTIAL/credentials") { + common.OutputErrorMessageToConsoleAndExit( + "fatal: cannot generate AK/SK if logged in via OIDC") + } + } + common.OutputErrorToConsoleAndExit(err) + } accessKeyFileContent := fmt.Sprintf( "export OS_ACCESS_KEY=%s\n"+ "export AWS_ACCESS_KEY_ID=%s\n"+ "export OS_SECRET_KEY=%s\n"+ "export AWS_SECRET_ACCESS_KEY=%s", - accessTokenCreationResponse.Credential.Access, - accessTokenCreationResponse.Credential.Access, - accessTokenCreationResponse.Credential.Secret, - accessTokenCreationResponse.Credential.Secret) + resp.AccessKey, + resp.AccessKey, + resp.SecretKey, + resp.SecretKey) common.WriteStringToFile("./ak-sk-env.sh", accessKeyFileContent) + log.Println("Access token file created successfully.") + log.Println("Please source the ak-sk-env.sh file in the current directory manually") +} - println("Access token file created successfully.") - println("Please source the ak-sk-env.sh file in the current directory manually") +func ListAccessToken() ([]credentials.Credential, error) { + client, err := getIdentityServiceClient() + if err != nil { + return nil, err + } + user, err := tokens.Get(client, config.GetActiveCloudConfig().UnscopedToken.Secret).ExtractUser() + if err != nil { + return nil, fmt.Errorf("couldn't get user: %w", err) + } + return credentials.List(client, credentials.ListOpts{UserID: user.ID}).Extract() } -func getAccessTokenFromServiceProvider(durationSeconds string) *http.Response { - secret := config.GetActiveCloudConfig().UnscopedToken.Secret - body := fmt.Sprintf("{\"auth\": {\"identity\": {\"methods\": [\"token\"], \"token\": {\"id\": \"%s\", \"duration_seconds\": \"%s\"}}}}", secret, durationSeconds) +func getAccessTokenFromServiceProvider(tokenDescription string) (*credentials.Credential, error) { + client, err := getIdentityServiceClient() + if err != nil { + return nil, err + } + user, err := tokens.Get(client, config.GetActiveCloudConfig().UnscopedToken.Secret).ExtractUser() + if err != nil { + return nil, fmt.Errorf("couldn't get user: %w", err) + } + credential, err := credentials.Create(client, credentials.CreateOpts{ + UserID: user.ID, + Description: tokenDescription, + }).Extract() - request := common.GetRequest(http.MethodPost, endpoints.IamSecurityTokens, strings.NewReader(body)) - request.Header.Add(headers.ContentType, headervalues.ApplicationJson) - request.Header.Add(xheaders.XAuthToken, secret) + var badRequest golangsdk.ErrDefault400 + if errors.As(err, &badRequest) { + accessTokens, listErr := ListAccessToken() + if listErr != nil { + return nil, listErr + } + + //nolint:gomnd // The OpenTelekomCloud only lets users have up to two keys + if len(accessTokens) == 2 { + log.Printf("Hit the limit for access keys on OTC. You can only have 2. Removing keys made by otc-auth...") + return conditionallyReplaceAccessTokens(user, client, tokenDescription, accessTokens) + } + return nil, err + } + return credential, err +} + +// Replaces AK/SKs made by otc-auth if their descriptions match the default.. +func conditionallyReplaceAccessTokens(user *tokens.User, client *golangsdk.ServiceClient, + tokenDescription string, accessTokens []credentials.Credential, +) (*credentials.Credential, error) { + changed := false + for _, token := range accessTokens { + if token.Description == "Token by otc-auth" { + err := DeleteAccessToken(token.AccessKey) + if err != nil { + return nil, err + } + changed = true + break + } + } + + if changed { + return credentials.Create(client, credentials.CreateOpts{ + UserID: user.ID, + Description: tokenDescription, + }).Extract() + } + return nil, errors.New("fatal: couldn't find a token created by this tool to replace") +} + +func DeleteAccessToken(token string) error { + client, err := getIdentityServiceClient() + if err != nil { + return err + } + return credentials.Delete(client, token).ExtractErr() +} - return common.HttpClientMakeRequest(request) +func getIdentityServiceClient() (*golangsdk.ServiceClient, error) { + provider, err := openstack.AuthenticatedClient(golangsdk.AuthOptions{ + IdentityEndpoint: endpoints.BaseURLIam(config.GetActiveCloudConfig().Region) + "/v3", + DomainID: config.GetActiveCloudConfig().Domain.ID, + TokenID: config.GetActiveCloudConfig().UnscopedToken.Secret, + }) + if err != nil { + return nil, fmt.Errorf("couldn't get provider: %w", err) + } + return openstack.NewIdentityV3(provider, golangsdk.EndpointOpts{}) } diff --git a/argument_verifier.go b/argument_verifier.go index 76152030..27e67dd4 100644 --- a/argument_verifier.go +++ b/argument_verifier.go @@ -2,20 +2,23 @@ package main import ( "fmt" + "log" "os" - "otc-auth/common" "strings" + + "otc-auth/common" ) const ( envOsUsername = "OS_USERNAME" envOsPassword = "OS_PASSWORD" envOsDomainName = "OS_DOMAIN_NAME" - envOsUserDomainId = "OS_USER_DOMAIN_ID" + envRegion = "REGION" + envOsUserDomainID = "OS_USER_DOMAIN_ID" envOsProjectName = "OS_PROJECT_NAME" envIdpName = "IDP_NAME" - envIdpUrl = "IDP_URL" - envClientId = "CLIENT_ID" + envIdpURL = "IDP_URL" + envClientID = "CLIENT_ID" envClientSecret = "CLIENT_SECRET" envClusterName = "CLUSTER_NAME" envOidScopes = "OIDC_SCOPES" @@ -46,7 +49,7 @@ func getClusterNameOrThrow(clusterName string) string { func getIdpInfoOrThrow(provider string, url string) (string, string) { provider = checkIDPProviderIsSet(provider) - url = checkIdpUrlIsSet(url) + url = checkIdpURLIsSet(url) return provider, url } @@ -58,12 +61,12 @@ func checkIDPProviderIsSet(provider string) string { return getEnvironmentVariableOrThrow(idpName, envIdpName) } -func checkIdpUrlIsSet(url string) string { +func checkIdpURLIsSet(url string) string { if url != "" { return url } - return getEnvironmentVariableOrThrow(idpUrlArg, envIdpUrl) + return getEnvironmentVariableOrThrow(idpURLArg, envIdpURL) } func getUsernameOrThrow(username string) string { @@ -90,23 +93,31 @@ func getDomainNameOrThrow(domainName string) string { return getEnvironmentVariableOrThrow(osDomainName, envOsDomainName) } -func checkMFAFlowIAM(otp string, userId string) (string, string) { +func getRegionCodeOrThrow(regionCode string) string { + if regionCode != "" { + return regionCode + } + + return getEnvironmentVariableOrThrow(region, envRegion) +} + +func checkMFAFlowIAM(otp string, userID string) (string, string) { if otp != "" { - if userId != "" { - return otp, userId + if userID != "" { + return otp, userID } - userId = getEnvironmentVariableOrThrow(osUserDomainId, envOsUserDomainId) + userID = getEnvironmentVariableOrThrow(osUserDomainID, envOsUserDomainID) } - return otp, userId + return otp, userID } -func getClientIdOrThrow(id string) string { +func getClientIDOrThrow(id string) string { if id != "" { return id } - return getEnvironmentVariableOrThrow(clientIdArg, envClientId) + return getEnvironmentVariableOrThrow(clientIDArg, envClientID) } func findClientSecretOrReturnEmpty(secret string) string { @@ -115,7 +126,7 @@ func findClientSecretOrReturnEmpty(secret string) string { } else if secretEnvVar, ok := os.LookupEnv(envClientSecret); ok { return secretEnvVar } else { - println(fmt.Sprintf("info: argument --%s not set. Continuing...\n", clientSecretArg)) + log.Printf("info: argument --%s not set. Continuing...\n", clientSecretArg) return "" } } @@ -142,5 +153,7 @@ func getEnvironmentVariableOrThrow(argument string, envVarName string) string { } func noArgumentProvidedErrorMessage(argument string, environmentVariable string) string { - return fmt.Sprintf("fatal: %s not provided.\n\nPlease make sure the argument %s is provided or the environment variable %s is set.", argument, argument, environmentVariable) + return fmt.Sprintf( + "fatal: %s not provided.\n\nPlease make sure the argument %s is provided or the environment variable %s is set.", + argument, argument, environmentVariable) } diff --git a/cce/cce.go b/cce/cce.go index 81e421bd..089b1be0 100644 --- a/cce/cce.go +++ b/cce/cce.go @@ -1,35 +1,41 @@ package cce import ( + "encoding/json" "errors" "fmt" - "github.com/avast/retry-go" - "github.com/go-http-utils/headers" "log" - "net/http" + "strconv" + "strings" + "otc-auth/common" "otc-auth/common/endpoints" - "otc-auth/common/headervalues" - "otc-auth/common/xheaders" "otc-auth/config" - "otc-auth/iam" - "strings" - "time" + + golangsdk "github.com/opentelekomcloud/gophertelekomcloud" + "github.com/opentelekomcloud/gophertelekomcloud/openstack" + "github.com/opentelekomcloud/gophertelekomcloud/openstack/cce/v3/clusters" ) func GetClusterNames(projectName string) config.Clusters { - clustersResult := getClustersForProjectFromServiceProvider(projectName) - var clusters config.Clusters - for _, item := range clustersResult.Items { - clusters = append(clusters, config.Cluster{ + clustersResult, err := getClustersForProjectFromServiceProvider(projectName) + if err != nil { + common.OutputErrorToConsoleAndExit(err) + } + + var clustersArr config.Clusters + + for _, item := range clustersResult { + clustersArr = append(clustersArr, config.Cluster{ Name: item.Metadata.Name, - Id: item.Metadata.UID, + ID: item.Metadata.Id, }) } - config.UpdateClusters(clusters) - println(fmt.Sprintf("CCE Clusters for project %s:\n%s", projectName, strings.Join(clusters.GetClusterNames(), ",\n"))) - return clusters + config.UpdateClusters(clustersArr) + log.Printf("CCE Clusters for project %s:\n%s", projectName, strings.Join(clustersArr.GetClusterNames(), ",\n")) + + return clustersArr } func GetKubeConfig(configParams KubeConfigParams) { @@ -37,80 +43,85 @@ func GetKubeConfig(configParams KubeConfigParams) { mergeKubeConfig(configParams, kubeConfig) - println(fmt.Sprintf("Successfully fetched and merge kube config for cce cluster %s.", configParams.ClusterName)) + log.Printf("Successfully fetched and merge kube config for cce cluster %s. \n", configParams.ClusterName) } -func getClustersForProjectFromServiceProvider(projectName string) common.ClustersResponse { - clustersResponse := common.ClustersResponse{} +func getClustersForProjectFromServiceProvider(projectName string) ([]clusters.Clusters, error) { project := config.GetActiveCloudConfig().Projects.GetProjectByNameOrThrow(projectName) - - err := retry.Do( - func() error { - infoMessage := fmt.Sprintf("info: fetching clusters for project %s", projectName) - println(infoMessage) - request := common.GetRequest(http.MethodGet, endpoints.Clusters(project.Id), nil) - request.Header.Add(headers.ContentType, headervalues.ApplicationJson) - scopedToken := iam.GetScopedToken(projectName) - request.Header.Add(xheaders.XAuthToken, scopedToken.Secret) - - response := common.HttpClientMakeRequest(request) - bodyBytes := common.GetBodyBytesFromResponse(response) - - clustersResponse = *common.DeserializeJsonForType[common.ClustersResponse](bodyBytes) - return nil - }, retry.OnRetry(func(n uint, err error) { - log.Printf("#%d: %s\n", n, err) - }), - retry.DelayType(retry.FixedDelay), - retry.Delay(time.Second*2), - ) + cloud := config.GetActiveCloudConfig() + provider, err := openstack.AuthenticatedClient(golangsdk.AuthOptions{ + IdentityEndpoint: endpoints.BaseURLIam(cloud.Region) + "/v3", + DomainID: cloud.Domain.ID, + TokenID: project.ScopedToken.Secret, + TenantID: project.ID, + }) if err != nil { - common.OutputErrorToConsoleAndExit(err) + return nil, fmt.Errorf("couldn't get provider: %w", err) } - - return clustersResponse + client, err := openstack.NewCCE(provider, golangsdk.EndpointOpts{}) + if err != nil { + return nil, fmt.Errorf("couldn't get clusters for project: %w", err) + } + return clusters.List(client, clusters.ListOpts{}) } -func getClusterCertFromServiceProvider(projectName string, clusterId string, duration string) (response *http.Response) { - body := fmt.Sprintf("{\"duration\": %s}", duration) - projectId := config.GetActiveCloudConfig().Projects.GetProjectByNameOrThrow(projectName).Id - - request := common.GetRequest(http.MethodPost, endpoints.ClusterCert(projectId, clusterId), strings.NewReader(body)) - request.Header.Add(headers.ContentType, headervalues.ApplicationJson) - request.Header.Add(headers.Accept, headervalues.ApplicationJson) +func getClusterCertFromServiceProvider(projectName string, clusterID string, duration string) (KubeConfig, error) { project := config.GetActiveCloudConfig().Projects.GetProjectByNameOrThrow(projectName) - request.Header.Add(xheaders.XAuthToken, project.ScopedToken.Secret) - - response = common.HttpClientMakeRequest(request) + cloud := config.GetActiveCloudConfig() + provider, err := openstack.AuthenticatedClient(golangsdk.AuthOptions{ + IdentityEndpoint: endpoints.BaseURLIam(cloud.Region) + "/v3", + DomainID: cloud.Domain.ID, + TokenID: project.ScopedToken.Secret, + TenantID: project.ID, + }) + if err != nil { + common.OutputErrorToConsoleAndExit(err) + } + client, err := openstack.NewCCE(provider, golangsdk.EndpointOpts{}) + if err != nil { + common.OutputErrorToConsoleAndExit(err) + } - return response + var expOpts clusters.ExpirationOpts + expOpts.Duration, err = strconv.Atoi(duration) + if err != nil { + common.OutputErrorToConsoleAndExit(err) + } + cert := clusters.GetCertWithExpiration(client, clusterID, expOpts).Body + var extractedCert KubeConfig + err = json.Unmarshal(cert, &extractedCert) + return extractedCert, err } -func getClusterId(clusterName string, projectName string) (clusterId string, err error) { +func getClusterID(clusterName string, projectName string) (clusterID string, err error) { cloud := config.GetActiveCloudConfig() if cloud.Clusters.ContainsClusterByName(clusterName) { - return cloud.Clusters.GetClusterByNameOrThrow(clusterName).Id, nil + return cloud.Clusters.GetClusterByNameOrThrow(clusterName).ID, nil } - clustersResult := getClustersForProjectFromServiceProvider(projectName) + clustersResult, err := getClustersForProjectFromServiceProvider(projectName) + if err != nil { + common.OutputErrorToConsoleAndExit(err) + } - var clusters config.Clusters - for _, cluster := range clustersResult.Items { - clusters = append(clusters, config.Cluster{ + var clusterArr config.Clusters + for _, cluster := range clustersResult { + clusterArr = append(clusterArr, config.Cluster{ Name: cluster.Metadata.Name, - Id: cluster.Metadata.UID, + ID: cluster.Metadata.Id, }) } - println(fmt.Sprintf("Clusters for project %s:\n%s", projectName, strings.Join(clusters.GetClusterNames(), ",\n"))) + log.Printf("Clusters for project %s:\n%s", projectName, strings.Join(clusterArr.GetClusterNames(), ",\n")) - config.UpdateClusters(clusters) + config.UpdateClusters(clusterArr) cloud = config.GetActiveCloudConfig() if cloud.Clusters.ContainsClusterByName(clusterName) { - return cloud.Clusters.GetClusterByNameOrThrow(clusterName).Id, nil + return cloud.Clusters.GetClusterByNameOrThrow(clusterName).ID, nil } - errorMessage := fmt.Sprintf("cluster not found.\nhere's a list of valid clusters:\n%s", strings.Join(clusters.GetClusterNames(), ",\n")) - return clusterId, errors.New(errorMessage) + errorMessage := fmt.Sprintf("cluster not found.\nhere's a list of valid clusters:\n%s", + strings.Join(clusterArr.GetClusterNames(), ",\n")) + return clusterID, errors.New(errorMessage) } diff --git a/cce/kube_config.go b/cce/kube_config.go index bf81c06a..aea73d89 100644 --- a/cce/kube_config.go +++ b/cce/kube_config.go @@ -1,38 +1,49 @@ package cce import ( + "encoding/json" "fmt" - . "k8s.io/client-go/tools/clientcmd" - "k8s.io/client-go/util/homedir" + "log" "os" - "otc-auth/common" - "otc-auth/config" "path" "path/filepath" "strings" + + "otc-auth/common" + "otc-auth/config" + + "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/util/homedir" ) func getKubeConfig(kubeConfigParams KubeConfigParams) string { - println("Getting kube config...") + log.Println("Getting kube config...") - clusterId, err := getClusterId(kubeConfigParams.ClusterName, kubeConfigParams.ProjectName) + clusterID, err := getClusterID(kubeConfigParams.ClusterName, kubeConfigParams.ProjectName) if err != nil { common.OutputErrorToConsoleAndExit(err, "fatal: error receiving cluster id: %s") } - response := getClusterCertFromServiceProvider(kubeConfigParams.ProjectName, clusterId, kubeConfigParams.DaysValid) - - return string(common.GetBodyBytesFromResponse(response)) + response, err := getClusterCertFromServiceProvider(kubeConfigParams.ProjectName, clusterID, kubeConfigParams.DaysValid) + if err != nil { + common.OutputErrorToConsoleAndExit(err) + } + responseMarshalled, err := json.Marshal(response) + if err != nil { + common.OutputErrorToConsoleAndExit(err) + } + return string(responseMarshalled) } func mergeKubeConfig(configParams KubeConfigParams, kubeConfigData string) { - kubeConfigContextData := addContextInformationToKubeConfig(configParams.ProjectName, configParams.ClusterName, kubeConfigData) - currentConfig, err := NewDefaultClientConfigLoadingRules().GetStartingConfig() + kubeConfigContextData := addContextInformationToKubeConfig(configParams.ProjectName, + configParams.ClusterName, kubeConfigData) + currentConfig, err := clientcmd.NewDefaultClientConfigLoadingRules().GetStartingConfig() if err != nil { common.OutputErrorToConsoleAndExit(err) } - clientConfig, err := NewClientConfigFromBytes([]byte(kubeConfigContextData)) + clientConfig, err := clientcmd.NewClientConfigFromBytes([]byte(kubeConfigContextData)) if err != nil { common.OutputErrorToConsoleAndExit(err) } @@ -44,16 +55,16 @@ func mergeKubeConfig(configParams KubeConfigParams, kubeConfigData string) { filenameNewFile := "kubeConfig_new" filenameCurrentFile := "kubeConfig_current" - err = WriteToFile(kubeConfig, filenameNewFile) + err = clientcmd.WriteToFile(kubeConfig, filenameNewFile) if err != nil { common.OutputErrorToConsoleAndExit(err) } - err = WriteToFile(*currentConfig, filenameCurrentFile) + err = clientcmd.WriteToFile(*currentConfig, filenameCurrentFile) if err != nil { common.OutputErrorToConsoleAndExit(err) } - loadingRules := ClientConfigLoadingRules{ + loadingRules := clientcmd.ClientConfigLoadingRules{ Precedence: []string{filenameNewFile, filenameCurrentFile}, } @@ -61,13 +72,13 @@ func mergeKubeConfig(configParams KubeConfigParams, kubeConfigData string) { if err != nil { common.OutputErrorToConsoleAndExit(err) } - err = WriteToFile(*mergedConfig, determineTargetLocation(configParams.TargetLocation)) + err = clientcmd.WriteToFile(*mergedConfig, determineTargetLocation(configParams.TargetLocation)) if err != nil { common.OutputErrorToConsoleAndExit(err) } - os.RemoveAll(filenameNewFile) - os.RemoveAll(filenameCurrentFile) + _ = os.RemoveAll(filenameNewFile) + _ = os.RemoveAll(filenameCurrentFile) } func determineTargetLocation(targetLocation string) string { @@ -78,19 +89,21 @@ func determineTargetLocation(targetLocation string) string { common.OutputErrorMessageToConsoleAndExit(err.Error()) } return targetLocation - } else { - return defaultKubeConfigLocation } + return defaultKubeConfigLocation } func addContextInformationToKubeConfig(projectName string, clusterName string, kubeConfigData string) string { cloud := config.GetActiveCloudConfig() - kubeConfigData = strings.ReplaceAll(kubeConfigData, "internalCluster", fmt.Sprintf("%s/%s-intranet", projectName, clusterName)) + kubeConfigData = strings.ReplaceAll(kubeConfigData, "internalCluster", fmt.Sprintf("%s/%s-intranet", + projectName, clusterName)) kubeConfigData = strings.ReplaceAll(kubeConfigData, "externalCluster", fmt.Sprintf("%s/%s", projectName, clusterName)) - kubeConfigData = strings.ReplaceAll(kubeConfigData, "internal", fmt.Sprintf("%s/%s-intranet", projectName, clusterName)) + kubeConfigData = strings.ReplaceAll(kubeConfigData, "internal", fmt.Sprintf("%s/%s-intranet", projectName, + clusterName)) kubeConfigData = strings.ReplaceAll(kubeConfigData, "external", fmt.Sprintf("%s/%s", projectName, clusterName)) - kubeConfigData = strings.ReplaceAll(kubeConfigData, ":\"user\"", fmt.Sprintf(":\"%s\"", cloud.Username)) + kubeConfigData = strings.ReplaceAll(kubeConfigData, ":\"user\"", + fmt.Sprintf(":\"%s-%s-%s\"", projectName, clusterName, cloud.Username)) return kubeConfigData } diff --git a/cce/model.go b/cce/model.go index 17b5fd9b..e2656cd3 100644 --- a/cce/model.go +++ b/cce/model.go @@ -15,12 +15,9 @@ type KubeConfig struct { Name string `json:"name"` Cluster struct { Server string `json:"server"` - CertificateAuthorityData string `json:"certificate-authority-data"` + CertificateAuthorityData string `json:"certificate-authority-data,omitempty"` + InsecureSkipTLSVerify bool `json:"insecure-skip-tls-verify,omitempty"` } `json:"cluster,omitempty"` - Cluster0 struct { - Server string `json:"server"` - InsecureSkipTLSVerify bool `json:"insecure-skip-tls-verify"` - } `json:"cluster0,omitempty"` } `json:"clusters"` Users []struct { Name string `json:"name"` diff --git a/common/endpoints/endpoints.go b/common/endpoints/endpoints.go index 2f002300..fe1fbf47 100644 --- a/common/endpoints/endpoints.go +++ b/common/endpoints/endpoints.go @@ -1,36 +1,25 @@ package endpoints import ( + "errors" "fmt" -) -const ( - BaseUrlIam = "https://iam.eu-de.otc.t-systems.com:443" - baseUrlCce = "https://cce.eu-de.otc.t-systems.com:443" - protocols = "protocols" - auth = "auth" + "otc-auth/common" ) -var ( - IamProjects = fmt.Sprintf("%s/v3/projects", BaseUrlIam) - IamTokens = fmt.Sprintf("%s/v3/auth/tokens", BaseUrlIam) - IamSecurityTokens = fmt.Sprintf("%s/v3.0/OS-CREDENTIAL/securitytokens", BaseUrlIam) - identityProviders = fmt.Sprintf("%s/v3/OS-FEDERATION/identity_providers", BaseUrlIam) - - cceProjects = fmt.Sprintf("%s/api/v3/projects", baseUrlCce) - clusters = "clusters" - clusterCert = "clustercert" +const ( + protocols = "protocols" + auth = "auth" ) -func IdentityProviders(identityProvider string, protocol string) string { - return fmt.Sprintf("%s/%s/%s/%s/%s", identityProviders, identityProvider, protocols, protocol, auth) -} - -func Clusters(projectId string) string { - return fmt.Sprintf("%s/%s/%s", cceProjects, projectId, clusters) +func BaseURLIam(region string) string { + if region == "" { + common.OutputErrorToConsoleAndExit(errors.New("empty region supplied, can't generate IAM URL")) + } + return fmt.Sprintf("https://iam.%s.otc.t-systems.com:443", region) } -func ClusterCert(projectId string, clusterId string) string { - clusters := Clusters(projectId) - return fmt.Sprintf("%s/%s/%s", clusters, clusterId, clusterCert) +func IdentityProviders(identityProvider string, protocol string, region string) string { + identityProviders := fmt.Sprintf("%s/v3/OS-FEDERATION/identity_providers", BaseURLIam(region)) + return fmt.Sprintf("%s/%s/%s/%s/%s", identityProviders, identityProvider, protocols, protocol, auth) } diff --git a/common/functions.go b/common/functions.go index ddd75cb2..8ec03d59 100644 --- a/common/functions.go +++ b/common/functions.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "fmt" + "log" "os" ) @@ -30,14 +31,14 @@ func WriteStringToFile(filepath string, content string) { func OutputErrorToConsoleAndExit(err error, errorMessage ...string) { if errorMessage != nil { - _, err := fmt.Fprintf(os.Stderr, errorMessage[0], err) - if err != nil { - OutputErrorToConsoleAndExit(err) + _, errPrint := fmt.Fprintf(os.Stderr, errorMessage[0], err) + if errPrint != nil { + OutputErrorToConsoleAndExit(errPrint) } } else { - _, err := fmt.Fprintf(os.Stderr, "fatal: %s", err) - if err != nil { - OutputErrorToConsoleAndExit(err) + _, errPrint := fmt.Fprintf(os.Stderr, "fatal: %s", err) + if errPrint != nil { + OutputErrorToConsoleAndExit(errPrint) } } @@ -45,20 +46,20 @@ func OutputErrorToConsoleAndExit(err error, errorMessage ...string) { } func OutputErrorMessageToConsoleAndExit(errorMessage string) { - fmt.Println(errorMessage) + log.Println(errorMessage) os.Exit(1) } -func ByteSliceToIndentedJsonFormat(biteSlice []byte) string { - var formattedJson bytes.Buffer - err := json.Indent(&formattedJson, biteSlice, "", " ") +func ByteSliceToIndentedJSONFormat(biteSlice []byte) string { + var formattedJSON bytes.Buffer + err := json.Indent(&formattedJSON, biteSlice, "", " ") if err != nil { OutputErrorToConsoleAndExit(err) } - return formattedJson.String() + return formattedJSON.String() } -func DeserializeJsonForType[T any](data []byte) *T { +func DeserializeJSONForType[T any](data []byte) *T { var pointer T err := json.Unmarshal(data, &pointer) if err != nil { diff --git a/common/headervalues/values.go b/common/headervalues/values.go index 7745c48d..1a41f534 100644 --- a/common/headervalues/values.go +++ b/common/headervalues/values.go @@ -1,8 +1,7 @@ package headervalues const ( - TextXml = "text/xml" + TextXML = "text/xml" ApplicationPaos = "application/vnd.paos+xml" - ApplicationJson = "application/json" Paos = `ver="urn:liberty:paos:2003-08";"urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp"` ) diff --git a/common/http_client.go b/common/http_client.go index d2871136..fc5bc878 100644 --- a/common/http_client.go +++ b/common/http_client.go @@ -8,7 +8,7 @@ import ( "strconv" ) -func HttpClientMakeRequest(request *http.Request) *http.Response { +func HTTPClientMakeRequest(request *http.Request) *http.Response { httpClient := http.Client{} response, err := httpClient.Do(request) if err != nil { @@ -20,9 +20,11 @@ func HttpClientMakeRequest(request *http.Request) *http.Response { } func GetRequest(method string, url string, body io.Reader) *http.Request { - request, err := http.NewRequest(method, url, body) + request, err := http.NewRequest(method, url, body) //nolint:noctx // This method will be removed soon anyway if err != nil { - OutputErrorMessageToConsoleAndExit(fmt.Sprintf("fatal: error building %s request for url %s\ntrace: %s", method, url, err.Error())) + OutputErrorMessageToConsoleAndExit(fmt.Sprintf( + "fatal: error building %s request for url %s\ntrace: %s", + method, url, err.Error())) } return request @@ -43,7 +45,7 @@ func GetBodyBytesFromResponse(response *http.Response) []byte { statusCodeStartsWith2 := regexp.MustCompile(`2\d{2}`) if !statusCodeStartsWith2.MatchString(strconv.Itoa(response.StatusCode)) { - errorMessage := fmt.Sprintf("error: status %s, body:\n%s", response.Status, ByteSliceToIndentedJsonFormat(bodyBytes)) + errorMessage := fmt.Sprintf("error: status %s, body:\n%s", response.Status, ByteSliceToIndentedJSONFormat(bodyBytes)) OutputErrorMessageToConsoleAndExit(errorMessage) } diff --git a/common/model.go b/common/model.go index 2f9afdea..81f0a982 100644 --- a/common/model.go +++ b/common/model.go @@ -5,16 +5,17 @@ import ( ) type AuthInfo struct { + Region string AuthType string IdpName string - IdpUrl string + IdpURL string Username string Password string AuthProtocol string DomainName string Otp string - UserDomainId string - ClientId string + UserDomainID string + ClientID string ClientSecret string OverwriteFile bool IsServiceAccount bool @@ -43,7 +44,7 @@ type TokenResponse struct { IssuedAt string `json:"issued_at"` User struct { Domain struct { - Id string `json:"id"` + ID string `json:"id"` Name string `json:"name"` } `json:"domain"` Name string `json:"name"` @@ -54,20 +55,12 @@ type TokenResponse struct { type ProjectsResponse struct { Projects []struct { Name string `json:"name"` - Id string `json:"id"` + ID string `json:"id"` } `json:"projects"` } -type ClustersResponse struct { - Items []struct { - Metadata struct { - Name string `json:"name"` - UID string `json:"uid"` - } `json:"metadata"` - } `json:"items"` -} - -const SuccessPageHtml = ` +//nolint:lll // This should probably be moved to its own file sometime +const SuccessPageHTML = ` diff --git a/common/otc_auth_helper.go b/common/otc_auth_helper.go index afb9019b..81291d4a 100644 --- a/common/otc_auth_helper.go +++ b/common/otc_auth_helper.go @@ -3,28 +3,34 @@ package common import ( "fmt" "net/http" - "otc-auth/common/xheaders" "strings" "time" + + "otc-auth/common/xheaders" ) const PrintTimeFormat = time.RFC1123 -func GetCloudCredentialsFromResponseOrThrow(response *http.Response) (tokenResponse TokenResponse) { +func GetCloudCredentialsFromResponseOrThrow(response *http.Response) TokenResponse { + var tokenResponse TokenResponse unscopedToken := response.Header.Get(xheaders.XSubjectToken) if unscopedToken == "" { bodyBytes := GetBodyBytesFromResponse(response) responseString := string(bodyBytes) if strings.Contains(responseString, "mfa totp code verify fail") { - OutputErrorMessageToConsoleAndExit("fatal: invalid otp unscopedToken.\n\nPlease try it again with a new otp unscopedToken.") + OutputErrorMessageToConsoleAndExit( + "fatal: invalid otp unscopedToken.\n" + + "\nPlease try it again with a new otp unscopedToken.") } else { - formattedError := ByteSliceToIndentedJsonFormat(bodyBytes) - OutputErrorMessageToConsoleAndExit(fmt.Sprintf("fatal: response failed with status %s. Body:\n%s", response.Status, formattedError)) + formattedError := ByteSliceToIndentedJSONFormat(bodyBytes) + OutputErrorMessageToConsoleAndExit(fmt.Sprintf( + "fatal: response failed with status %s. Body:\n%s", + response.Status, formattedError)) } } bodyBytes := GetBodyBytesFromResponse(response) - tokenResponse = *DeserializeJsonForType[TokenResponse](bodyBytes) + tokenResponse = *DeserializeJSONForType[TokenResponse](bodyBytes) tokenResponse.Token.Secret = unscopedToken return tokenResponse diff --git a/common/responses.go b/common/responses.go deleted file mode 100644 index 805d0c79..00000000 --- a/common/responses.go +++ /dev/null @@ -1 +0,0 @@ -package common diff --git a/common/xheaders/headers.go b/common/xheaders/headers.go index 1372f6da..a69eef27 100644 --- a/common/xheaders/headers.go +++ b/common/xheaders/headers.go @@ -3,6 +3,5 @@ package xheaders const ( Paos = "PAOS" - XAuthToken = "X-Auth-Token" XSubjectToken = "X-Subject-Token" ) diff --git a/config/config.go b/config/config.go index d8cc46eb..b95cd71c 100644 --- a/config/config.go +++ b/config/config.go @@ -4,13 +4,13 @@ import ( "bufio" "encoding/json" "fmt" + "log" "os" - "otc-auth/common" "path" "time" -) -var otcConfigPath = path.Join(GetHomeFolder(), ".otc-auth-config") + "otc-auth/common" +) func LoadCloudConfig(domainName string) { otcConfig := getOtcConfig() @@ -22,10 +22,7 @@ func LoadCloudConfig(domainName string) { otcConfig.Clouds = clouds writeOtcConfigContentToFile(otcConfig) - _, err := fmt.Fprintf(os.Stdout, "Cloud %s loaded successfully and set to active.\n", domainName) - if err != nil { - common.OutputErrorToConsoleAndExit(err) - } + log.Printf("Cloud %s loaded successfully and set to active.\n", domainName) } func registerNewCloud(domainName string) Clouds { @@ -33,12 +30,15 @@ func registerNewCloud(domainName string) Clouds { clouds := otcConfig.Clouds newCloud := Cloud{ - Domain: NameAndIdResource{ + Domain: NameAndIDResource{ Name: domainName, }, } if otcConfig.Clouds.ContainsCloud(newCloud.Domain.Name) { - common.OutputErrorMessageToConsoleAndExit(fmt.Sprintf("warning: cloud with name %s already exists.\n\nUse the cloud-config load command.", newCloud.Domain.Name)) + common.OutputErrorMessageToConsoleAndExit( + fmt.Sprintf("warning: cloud with name %s already exists.\n\nUse the cloud-config load command.", + newCloud.Domain.Name)) + return nil } @@ -57,7 +57,8 @@ func IsAuthenticationValid() bool { tokenExpirationDate := common.ParseTimeOrThrow(unscopedToken.ExpiresAt) if tokenExpirationDate.After(time.Now()) { // token still valid - println(fmt.Sprintf("info: unscoped token valid until %s", tokenExpirationDate.Format(common.PrintTimeFormat))) + log.Printf("info: unscoped token valid until %s", tokenExpirationDate.Format(common.PrintTimeFormat)) + return true } @@ -68,7 +69,8 @@ func IsAuthenticationValid() bool { func RemoveCloudConfig(domainName string) { otcConfig := getOtcConfig() if !otcConfig.Clouds.ContainsCloud(domainName) { - common.OutputErrorMessageToConsoleAndExit(fmt.Sprintf("fatal: cloud with name %s does not exist in the config file.", domainName)) + common.OutputErrorMessageToConsoleAndExit( + fmt.Sprintf("fatal: cloud with name %s does not exist in the config file.", domainName)) } removeCloudConfig(domainName) @@ -106,13 +108,15 @@ func GetActiveCloudConfig() Cloud { clouds := otcConfig.Clouds cloud, _, err := clouds.FindActiveCloudConfigOrNil() if err != nil { - common.OutputErrorToConsoleAndExit(err, "fatal: %s.\n\nPlease use the cloud-config register or the cloud-config load command to set an active cloud configuration.") + common.OutputErrorToConsoleAndExit(err, + "fatal: %s.\n\nPlease use the cloud-config register or the cloud-config load command "+ + "to set an active cloud configuration.") } return *cloud } func OtcConfigFileExists() bool { - fileInfo, err := os.Stat(otcConfigPath) + fileInfo, err := os.Stat(path.Join(GetHomeFolder(), ".otc-auth-config")) if err != nil && os.IsNotExist(err) { return false } @@ -123,7 +127,7 @@ func OtcConfigFileExists() bool { func getOtcConfig() OtcConfigContent { if !OtcConfigFileExists() { createConfigFileWithCloudConfig(OtcConfigContent{}) - println("info: cloud config created.") + log.Println("info: cloud config created.") } var otcConfig OtcConfigContent @@ -155,18 +159,18 @@ func writeOtcConfigContentToFile(content OtcConfigContent) { common.OutputErrorToConsoleAndExit(err, "fatal: error encoding json.\ntrace: %s") } - WriteConfigFile(common.ByteSliceToIndentedJsonFormat(contentAsBytes), otcConfigPath) + WriteConfigFile(common.ByteSliceToIndentedJSONFormat(contentAsBytes), path.Join(GetHomeFolder(), ".otc-auth-config")) } func readFileContent() string { - file, err := os.Open(otcConfigPath) + file, err := os.Open(path.Join(GetHomeFolder(), ".otc-auth-config")) if err != nil { common.OutputErrorToConsoleAndExit(err, "fatal: error opening config file.\ntrace: %s") } defer func(file *os.File) { - err := file.Close() - if err != nil { - common.OutputErrorToConsoleAndExit(err, "fatal: error saving config file.\ntrace: %s") + errClose := file.Close() + if errClose != nil { + common.OutputErrorToConsoleAndExit(errClose, "fatal: error saving config file.\ntrace: %s") } }(file) @@ -175,8 +179,8 @@ func readFileContent() string { for fileScanner.Scan() { content += fileScanner.Text() } - if err := fileScanner.Err(); err != nil { - common.OutputErrorToConsoleAndExit(err, "fatal: error reading config file.\ntrace: %s") + if errScanner := fileScanner.Err(); errScanner != nil { + common.OutputErrorToConsoleAndExit(errScanner, "fatal: error reading config file.\ntrace: %s") } return content diff --git a/config/model.go b/config/model.go index 2e3526f7..399598dd 100644 --- a/config/model.go +++ b/config/model.go @@ -1,15 +1,10 @@ package config import ( - "errors" "fmt" - "otc-auth/common" "time" -) -const ( - Unscoped = "unscoped" - Scoped = "scoped" + "otc-auth/common" ) type OtcConfigContent struct { @@ -27,15 +22,6 @@ func (clouds *Clouds) ContainsCloud(name string) bool { return false } -func (clouds *Clouds) GetCloudByName(name string) *Cloud { - for _, cloud := range *clouds { - if cloud.Domain.Name == name { - return &cloud - } - } - return nil -} - func (clouds *Clouds) RemoveCloudByNameIfExists(name string) { for index, cloud := range *clouds { if cloud.Domain.Name == name { @@ -51,13 +37,12 @@ func (clouds *Clouds) SetActiveByName(name string) { } else { (*clouds)[index].Active = false } - } } func (clouds *Clouds) FindActiveCloudConfigOrNil() (cloud *Cloud, index *int, err error) { if clouds.NumberOfActiveCloudConfigs() > 1 { - return nil, nil, errors.New("more than one cloud active") + return nil, nil, fmt.Errorf("more than one cloud active") } for index, cloud := range *clouds { @@ -66,16 +51,7 @@ func (clouds *Clouds) FindActiveCloudConfigOrNil() (cloud *Cloud, index *int, er } } - return nil, nil, errors.New("no active cloud") -} - -func (clouds *Clouds) GetActiveCloud() *Cloud { - cloud, _, err := clouds.FindActiveCloudConfigOrNil() - if err != nil || cloud == nil { - common.OutputErrorToConsoleAndExit(err, "fatal: invalid state %s") - } - - return cloud + return nil, nil, fmt.Errorf("no active cloud") } func (clouds *Clouds) GetActiveCloudIndex() int { @@ -98,7 +74,8 @@ func (clouds *Clouds) NumberOfActiveCloudConfigs() int { } type Cloud struct { - Domain NameAndIdResource `json:"domain"` + Region string `json:"region"` + Domain NameAndIDResource `json:"domain"` UnscopedToken Token `json:"unscopedToken"` Projects Projects `json:"projects"` Clusters Clusters `json:"clusters"` @@ -107,7 +84,7 @@ type Cloud struct { } type Project struct { - NameAndIdResource + NameAndIDResource ScopedToken Token `json:"scopedToken"` } type Projects []Project @@ -124,8 +101,9 @@ func (projects Projects) FindProjectByName(name string) *Project { func (projects Projects) GetProjectByNameOrThrow(name string) Project { project := projects.FindProjectByName(name) if project == nil { - errorMessage := fmt.Sprintf("fatal: project with name %s not found.\n\nUse the cce list-projects command to get a list of projects.", name) - common.OutputErrorToConsoleAndExit(errors.New(errorMessage)) + common.OutputErrorToConsoleAndExit(fmt.Errorf( + "fatal: project with name %s not found.\n\nUse the cce list-projects command to "+ + "get a list of projects", name)) } return *project } @@ -139,17 +117,21 @@ func (projects Projects) FindProjectIndexByName(name string) *int { return nil } -func (projects Projects) GetProjectNames() (names []string) { +func (projects Projects) GetProjectNames() []string { + var names []string for _, project := range projects { names = append(names, project.Name) } return names } -type Cluster NameAndIdResource -type Clusters []Cluster +type ( + Cluster NameAndIDResource + Clusters []Cluster +) -func (clusters Clusters) GetClusterNames() (names []string) { +func (clusters Clusters) GetClusterNames() []string { + var names []string for _, cluster := range clusters { names = append(names, cluster.Name) } @@ -159,8 +141,9 @@ func (clusters Clusters) GetClusterNames() (names []string) { func (clusters Clusters) GetClusterByNameOrThrow(name string) Cluster { cluster := clusters.FindClusterByName(name) if cluster == nil { - errorMessage := fmt.Sprintf("fatal: cluster with name %s not found.\nuse the cce list-clusters command to retrieve a list of clusters.", name) - common.OutputErrorToConsoleAndExit(errors.New(errorMessage)) + common.OutputErrorToConsoleAndExit(fmt.Errorf( + "fatal: cluster with name %s not found.\nuse the cce list-clusters command to retrieve "+ + "a list of clusters", name)) } return *cluster } @@ -175,16 +158,12 @@ func (clusters Clusters) FindClusterByName(name string) *Cluster { } func (clusters Clusters) ContainsClusterByName(name string) bool { - if clusters.FindClusterByName(name) == nil { - return false - } else { - return true - } + return clusters.FindClusterByName(name) != nil } -type NameAndIdResource struct { +type NameAndIDResource struct { Name string `json:"name"` - Id string `json:"id"` + ID string `json:"id"` } type Token struct { @@ -193,14 +172,8 @@ type Token struct { ExpiresAt string `json:"expires_at"` } -type Tokens []Token - func (token *Token) IsTokenValid() bool { - if common.ParseTimeOrThrow(token.ExpiresAt).After(time.Now()) { - return true - } else { - return false - } + return common.ParseTimeOrThrow(token.ExpiresAt).After(time.Now()) } func (token *Token) UpdateToken(updatedToken Token) Token { diff --git a/format_and_lint.sh b/format_and_lint.sh new file mode 100644 index 00000000..8bd3fb05 --- /dev/null +++ b/format_and_lint.sh @@ -0,0 +1,8 @@ +#!/bin/zsh +reset && \ +gci write . && \ +go vet . && \ +goimports -w . && \ +gofmt -w . && \ +gofumpt -w . && \ +$HOME/go/bin/golangci-lint run -v \ No newline at end of file diff --git a/go.mod b/go.mod index b7a5d905..25ff1288 100644 --- a/go.mod +++ b/go.mod @@ -4,11 +4,11 @@ go 1.19 require ( github.com/akamensky/argparse v1.4.0 - github.com/avast/retry-go v3.0.0+incompatible github.com/coreos/go-oidc/v3 v3.4.0 github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a github.com/google/uuid v1.1.2 github.com/gophercloud/utils v0.0.0-20230330070308-5bd5e1d608f8 + github.com/opentelekomcloud/gophertelekomcloud v0.6.1 github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1 gopkg.in/yaml.v2 v2.4.0 @@ -31,7 +31,7 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/testify v1.8.0 // indirect - golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0 // indirect + golang.org/x/crypto v0.1.0 // indirect golang.org/x/net v0.7.0 // indirect golang.org/x/sys v0.5.0 // indirect golang.org/x/term v0.5.0 // indirect diff --git a/go.sum b/go.sum index e4d2f11b..7991f9b8 100644 --- a/go.sum +++ b/go.sum @@ -62,8 +62,6 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV github.com/akamensky/argparse v1.4.0 h1:YGzvsTqCvbEZhL8zZu2AiA5nq805NZh75JNj4ajn1xc= github.com/akamensky/argparse v1.4.0/go.mod h1:S5kwC7IuDcEr5VeXtGPRVZ5o/FdhcMlQz4IZQuw64xA= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0= -github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= @@ -233,6 +231,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/opentelekomcloud/gophertelekomcloud v0.6.1 h1:au6sQ6RbJJ16gcpRAL+o0pkb2C23zHmNoAvjpPzo0Gg= +github.com/opentelekomcloud/gophertelekomcloud v0.6.1/go.mod h1:9Deb3q2gJvq5dExV+aX+iO+G+mD9Zr9uFt+YY9ONmq0= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -278,8 +278,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0 h1:a5Yg6ylndHHYJqIPrdq0AhvR6KTvDTAvgBtaidhEevY= -golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= +golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -362,6 +362,7 @@ golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -461,11 +462,13 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/iam/iam.go b/iam/iam.go index b8ec7fe6..b62d4453 100644 --- a/iam/iam.go +++ b/iam/iam.go @@ -1,30 +1,55 @@ package iam import ( - "errors" + "encoding/json" "fmt" - "github.com/avast/retry-go" - "github.com/go-http-utils/headers" "log" - "net/http" + "time" + "otc-auth/common" "otc-auth/common/endpoints" - "otc-auth/common/headervalues" - "otc-auth/common/xheaders" "otc-auth/config" - "strings" - "time" + + golangsdk "github.com/opentelekomcloud/gophertelekomcloud" + "github.com/opentelekomcloud/gophertelekomcloud/openstack" + "github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/tokens" ) func AuthenticateAndGetUnscopedToken(authInfo common.AuthInfo) common.TokenResponse { - requestBody := getRequestBodyForAuthenticationMethod(authInfo) - request := common.GetRequest(http.MethodPost, endpoints.IamTokens, strings.NewReader(requestBody)) - request.Header.Add(headers.ContentType, headervalues.ApplicationJson) + authOpts := golangsdk.AuthOptions{ + DomainName: authInfo.DomainName, + Username: authInfo.Username, + Password: authInfo.Password, + IdentityEndpoint: endpoints.BaseURLIam(authInfo.Region) + "/v3", + + Passcode: authInfo.Otp, + UserID: authInfo.UserDomainID, + } + + provider, err := openstack.AuthenticatedClient(authOpts) + if err != nil { + common.OutputErrorToConsoleAndExit(err) + } + + client, err := openstack.NewIdentityV3(provider, golangsdk.EndpointOpts{}) + if err != nil { + common.OutputErrorToConsoleAndExit(err) + } + + tokenResult := tokens.Create(client, &authOpts) - response := common.HttpClientMakeRequest(request) - defer response.Body.Close() + var tokenMarshalledResult common.TokenResponse + err = json.Unmarshal(tokenResult.Body, &tokenMarshalledResult) + if err != nil { + common.OutputErrorToConsoleAndExit(err) + } - return common.GetCloudCredentialsFromResponseOrThrow(response) + token, err := tokenResult.ExtractToken() + if err != nil { + common.OutputErrorToConsoleAndExit(err) + } + tokenMarshalledResult.Token.Secret = token.ID + return tokenMarshalledResult } func GetScopedToken(projectName string) config.Token { @@ -35,80 +60,55 @@ func GetScopedToken(projectName string) config.Token { tokenExpirationDate := common.ParseTimeOrThrow(token.ExpiresAt) if tokenExpirationDate.After(time.Now()) { - println(fmt.Sprintf("info: scoped token is valid until %s", tokenExpirationDate.Format(common.PrintTimeFormat))) + log.Printf("info: scoped token is valid until %s \n", tokenExpirationDate.Format(common.PrintTimeFormat)) return token } } - println("attempting to request a scoped token.") - getScopedTokenFromServiceProvider(projectName) + log.Println("attempting to request a scoped token.") + cloud := getCloudWithScopedTokenFromServiceProvider(projectName) + config.UpdateCloudConfig(cloud) + log.Println("scoped token acquired successfully.") project = config.GetActiveCloudConfig().Projects.GetProjectByNameOrThrow(projectName) return project.ScopedToken } -func getScopedTokenFromServiceProvider(projectName string) { +func getCloudWithScopedTokenFromServiceProvider(projectName string) config.Cloud { cloud := config.GetActiveCloudConfig() - projectId := cloud.Projects.GetProjectByNameOrThrow(projectName).Id - - err := retry.Do( - func() error { - requestBody := fmt.Sprintf("{\"auth\": {\"identity\": {\"methods\": [\"token\"], \"token\": {\"id\": \"%s\"}}, \"scope\": {\"project\": {\"id\": \"%s\"}}}}", cloud.UnscopedToken.Secret, projectId) - - request := common.GetRequest(http.MethodPost, endpoints.IamTokens, strings.NewReader(requestBody)) - request.Header.Add(headers.ContentType, headervalues.ApplicationJson) - - response := common.HttpClientMakeRequest(request) - - scopedToken := response.Header.Get(xheaders.XSubjectToken) - - if scopedToken == "" { - bodyBytes := common.GetBodyBytesFromResponse(response) - formattedError := common.ByteSliceToIndentedJsonFormat(bodyBytes) - defer response.Body.Close() - println("error: an error occurred while polling a scoped token. Will try again") - return fmt.Errorf("http status code: %s\nresponse body:\n%s", response.Status, formattedError) - } - - bodyBytes := common.GetBodyBytesFromResponse(response) - tokenResponse := common.DeserializeJsonForType[common.TokenResponse](bodyBytes) - defer response.Body.Close() - - token := config.Token{ - Secret: scopedToken, - IssuedAt: tokenResponse.Token.IssuedAt, - ExpiresAt: tokenResponse.Token.ExpiresAt, - } - index := cloud.Projects.FindProjectIndexByName(projectName) - if index == nil { - errorMessage := fmt.Sprintf("fatal: project with name %s not found.\n\nUse the cce list-projects command to get a list of projects.", projectName) - common.OutputErrorToConsoleAndExit(errors.New(errorMessage)) - } - cloud.Projects[*index].ScopedToken = token - config.UpdateCloudConfig(cloud) - println("scoped token acquired successfully.") - - return nil - }, retry.OnRetry(func(n uint, err error) { - log.Printf("#%d: %s\n", n, err) - }), - retry.DelayType(retry.FixedDelay), - retry.Delay(time.Second*5), - ) + projectID := cloud.Projects.GetProjectByNameOrThrow(projectName).ID + + authOpts := golangsdk.AuthOptions{ + IdentityEndpoint: endpoints.BaseURLIam(cloud.Region) + "/v3", + TokenID: cloud.UnscopedToken.Secret, + TenantID: projectID, + DomainName: cloud.Domain.Name, + } + + provider, err := openstack.AuthenticatedClient(authOpts) + if err != nil { + common.OutputErrorToConsoleAndExit(err) + } + client, err := openstack.NewIdentityV3(provider, golangsdk.EndpointOpts{}) + if err != nil { + common.OutputErrorToConsoleAndExit(err) + } + + scopedToken, err := tokens.Create(client, &authOpts).ExtractToken() if err != nil { common.OutputErrorToConsoleAndExit(err) } -} -func getRequestBodyForAuthenticationMethod(authInfo common.AuthInfo) (requestBody string) { - if authInfo.Otp != "" && authInfo.UserDomainId != "" { - requestBody = fmt.Sprintf("{\"auth\": {\"identity\": {\"methods\": [\"password\", \"totp\"], "+ - "\"password\": {\"user\": {\"name\": \"%s\", \"password\": \"%s\", \"domain\": {\"name\": \"%s\"}}}, "+ - "\"totp\" : {\"user\": {\"id\": \"%s\", \"passcode\": \"%s\"}}}, \"scope\": {\"domain\": {\"name\": \"%s\"}}}}", - authInfo.Username, authInfo.Password, authInfo.DomainName, authInfo.UserDomainId, authInfo.Otp, authInfo.DomainName) - } else { - requestBody = fmt.Sprintf("{\"auth\": {\"identity\": {\"methods\": [\"password\"], "+ - "\"password\": {\"user\": {\"name\": \"%s\", \"password\": \"%s\", \"domain\": {\"name\": \"%s\"}}}}, "+ - "\"scope\": {\"domain\": {\"name\": \"%s\"}}}}", authInfo.Username, authInfo.Password, authInfo.DomainName, authInfo.DomainName) + token := config.Token{ + Secret: scopedToken.ID, + ExpiresAt: scopedToken.ExpiresAt.Format(time.RFC3339), + } + index := cloud.Projects.FindProjectIndexByName(projectName) + if index == nil { + common.OutputErrorToConsoleAndExit( + fmt.Errorf("fatal: project with name %s not found.\n"+ + "\nUse the cce list-projects command to get a list of projects", + projectName)) } - return requestBody + cloud.Projects[*index].ScopedToken = token + return cloud } diff --git a/iam/projects.go b/iam/projects.go index 034653bf..86e445b6 100644 --- a/iam/projects.go +++ b/iam/projects.go @@ -1,29 +1,31 @@ package iam import ( - "fmt" - "github.com/go-http-utils/headers" - "net/http" + "encoding/json" + "log" + "strings" + "otc-auth/common" "otc-auth/common/endpoints" - "otc-auth/common/headervalues" - "otc-auth/common/xheaders" "otc-auth/config" - "strings" + + golangsdk "github.com/opentelekomcloud/gophertelekomcloud" + "github.com/opentelekomcloud/gophertelekomcloud/openstack" + "github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/projects" ) func GetProjectsInActiveCloud() config.Projects { projectsResponse := getProjectsFromServiceProvider() - var projects config.Projects + var cloudProjects config.Projects for _, project := range projectsResponse.Projects { - projects = append(projects, config.Project{ - NameAndIdResource: config.NameAndIdResource{Name: project.Name, Id: project.Id}, + cloudProjects = append(cloudProjects, config.Project{ + NameAndIDResource: config.NameAndIDResource{Name: project.Name, ID: project.ID}, }) } - config.UpdateProjects(projects) - println(fmt.Sprintf("Projects for active cloud:\n%s", strings.Join(projects.GetProjectNames(), ",\n"))) - return projects + config.UpdateProjects(cloudProjects) + log.Printf("Projects for active cloud:\n%s \n", strings.Join(cloudProjects.GetProjectNames(), ",\n")) + return cloudProjects } func CreateScopedTokenForEveryProject(projectNames []string) { @@ -34,15 +36,31 @@ func CreateScopedTokenForEveryProject(projectNames []string) { func getProjectsFromServiceProvider() (projectsResponse common.ProjectsResponse) { cloud := config.GetActiveCloudConfig() - println(fmt.Sprintf("info: fetching projects for cloud %s", cloud.Domain.Name)) + log.Printf("info: fetching projects for cloud %s \n", cloud.Domain.Name) - request := common.GetRequest(http.MethodGet, endpoints.IamProjects, nil) - request.Header.Add(headers.ContentType, headervalues.ApplicationJson) - request.Header.Add(xheaders.XAuthToken, cloud.UnscopedToken.Secret) + provider, err := openstack.AuthenticatedClient(golangsdk.AuthOptions{ + IdentityEndpoint: endpoints.BaseURLIam(cloud.Region) + "/v3", + DomainID: cloud.Domain.ID, + TokenID: cloud.UnscopedToken.Secret, + }) + if err != nil { + common.OutputErrorToConsoleAndExit(err) + } + client, err := openstack.NewIdentityV3(provider, golangsdk.EndpointOpts{}) + if err != nil { + common.OutputErrorToConsoleAndExit(err) + } + projectsList, err := projects.List(client, projects.ListOpts{}).AllPages() + if err != nil { + common.OutputErrorToConsoleAndExit(err) + } - response := common.HttpClientMakeRequest(request) - bodyBytes := common.GetBodyBytesFromResponse(response) - projectsResponse = *common.DeserializeJsonForType[common.ProjectsResponse](bodyBytes) + projectsResponseMap := projectsList.GetBody() + + err = json.Unmarshal(projectsResponseMap, &projectsResponse) + if err != nil { + common.OutputErrorToConsoleAndExit(err) + } return projectsResponse } diff --git a/login.go b/login.go index 31b969bd..ba3b5af9 100644 --- a/login.go +++ b/login.go @@ -1,6 +1,8 @@ package main import ( + "log" + "otc-auth/common" "otc-auth/config" "otc-auth/iam" @@ -12,35 +14,41 @@ func AuthenticateAndGetUnscopedToken(authInfo common.AuthInfo) { config.LoadCloudConfig(authInfo.DomainName) if config.IsAuthenticationValid() && !authInfo.OverwriteFile { - println("info: will not retrieve unscoped token, because the current one is still valid.\n\nTo overwrite the existing unscoped token, pass the \"--overwrite-token\" argument.") + log.Println( + "info: will not retrieve unscoped token, because the current one is still valid.\n" + + "\nTo overwrite the existing unscoped token, pass the \"--overwrite-token\" argument.") return } - println("Retrieving unscoped token for active cloud...") + log.Println("Retrieving unscoped token for active cloud...") var tokenResponse common.TokenResponse switch authInfo.AuthType { case "idp": - if authInfo.AuthProtocol == protocolSAML { + switch authInfo.AuthProtocol { + case protocolSAML: tokenResponse = saml.AuthenticateAndGetUnscopedToken(authInfo) - } else if authInfo.AuthProtocol == protocolOIDC { + case protocolOIDC: tokenResponse = oidc.AuthenticateAndGetUnscopedToken(authInfo) - } else { - common.OutputErrorMessageToConsoleAndExit("fatal: unsupported login protocol.\n\nAllowed values are \"saml\" or \"oidc\". Please provide a valid argument and try again.") + default: + common.OutputErrorMessageToConsoleAndExit( + "fatal: unsupported login protocol.\n\nAllowed values are \"saml\" or \"oidc\". " + + "Please provide a valid argument and try again.") } case "iam": tokenResponse = iam.AuthenticateAndGetUnscopedToken(authInfo) default: - common.OutputErrorMessageToConsoleAndExit("fatal: unsupported authorization type.\n\nAllowed values are \"idp\" or \"iam\". Please provide a valid argument and try again.") - + common.OutputErrorMessageToConsoleAndExit( + "fatal: unsupported authorization type.\n\nAllowed values are \"idp\" or \"iam\". " + + "Please provide a valid argument and try again.") } if tokenResponse.Token.Secret == "" { common.OutputErrorMessageToConsoleAndExit("Authorization did not succeed. Please try again.") } - updateOTCInfoFile(tokenResponse) + updateOTCInfoFile(tokenResponse, authInfo.Region) createScopedTokenForEveryProject() - println("Successfully obtained unscoped token!") + log.Println("Successfully obtained unscoped token!") } func createScopedTokenForEveryProject() { @@ -48,13 +56,13 @@ func createScopedTokenForEveryProject() { iam.CreateScopedTokenForEveryProject(projectsInActiveCloud.GetProjectNames()) } -func updateOTCInfoFile(tokenResponse common.TokenResponse) { +func updateOTCInfoFile(tokenResponse common.TokenResponse, regionCode string) { cloud := config.GetActiveCloudConfig() if cloud.Domain.Name != tokenResponse.Token.User.Domain.Name { // Sanity check: we're in the same cloud as the active cloud common.OutputErrorMessageToConsoleAndExit("fatal: authorization made for wrong cloud configuration") } - cloud.Domain.Id = tokenResponse.Token.User.Domain.Id + cloud.Domain.ID = tokenResponse.Token.User.Domain.ID if cloud.Username != tokenResponse.Token.User.Name { for i, project := range cloud.Projects { cloud.Projects[i].ScopedToken = project.ScopedToken.UpdateToken(config.Token{ @@ -70,7 +78,7 @@ func updateOTCInfoFile(tokenResponse common.TokenResponse) { IssuedAt: tokenResponse.Token.IssuedAt, ExpiresAt: tokenResponse.Token.ExpiresAt, } - + cloud.Region = regionCode cloud.UnscopedToken = token config.UpdateCloudConfig(cloud) } diff --git a/main.go b/main.go index af7eee1d..64c39cd7 100644 --- a/main.go +++ b/main.go @@ -2,17 +2,20 @@ package main import ( "fmt" - "github.com/akamensky/argparse" + "log" "os" + "otc-auth/accesstoken" "otc-auth/cce" "otc-auth/common" "otc-auth/config" "otc-auth/iam" "otc-auth/openstack" + + "github.com/akamensky/argparse" ) -// GoReleaser will set the following 2 ldflags by default +//nolint:gochecknoglobals // Globals needed for GoReleaser which will set the following 2 ldflags by default. var ( version = "dev" date = "unknown" @@ -23,36 +26,44 @@ const ( osPassword = "os-password" overwriteTokenArg = "overwrite-token" osDomainName = "os-domain-name" - osUserDomainId = "os-user-domain-id" + region = "region" + osUserDomainID = "os-user-domain-id" osProjectName = "os-project-name" totpArg = "totp" idpName = "idp-name" - idpUrlArg = "idp-url" - clientIdArg = "client-id" + idpURLArg = "idp-url" + clientIDArg = "client-id" clientSecretArg = "client-secret" clusterArg = "cluster" isServiceAccountArg = "service-account" oidcScopesArg = "oidc-scopes" ) +//nolint:funlen,gocognit func main() { + log.SetFlags(0) // Remove timestamps from printed messages const ( provideArgumentHelp = "Either provide this argument or set the environment variable" - overwriteTokenHelp = "Overrides .otc-info file" + overwriteTokenHelp = "Overrides .otc-info file" //nolint:gosec // gosec thinks these are hardcoded credentials requiredForIdp = "Required for authentication with IdP." ) var ( - domainName *string - username *string - password *string - overwriteToken *bool - identityProvider *string - identityProviderUrl *string - isServiceAccount *bool - idpCommandHelp = fmt.Sprintf("The name of the identity provider. Allowed values in the iam section of the OTC UI. %s %s %s", requiredForIdp, provideArgumentHelp, envIdpName) - idpUrlCommandHelp = fmt.Sprintf("Url from the identity provider (e.g. ...realms/myrealm/protocol/saml). %s %s %s", requiredForIdp, provideArgumentHelp, envIdpUrl) - isServiceAccountHelp = "Flag to set if the account is a service account. The service account needs to be configured in your identity provider." - oidcScopesHelp = "Flag to set the scopes which are expected from the OIDC request." + domainName *string + regionCode *string + username *string + password *string + overwriteToken *bool + identityProvider *string + identityProviderURL *string + isServiceAccount *bool + idpCommandHelp = fmt.Sprintf( + "The name of the identity provider. Allowed values in the iam section of the OTC UI. %s %s %s", + requiredForIdp, provideArgumentHelp, envIdpName) + idpURLCommandHelp = fmt.Sprintf("Url from the identity provider (e.g. ...realms/myrealm/protocol/saml). %s %s %s", + requiredForIdp, provideArgumentHelp, envIdpURL) + isServiceAccountHelp = "Flag to set if the account is a service account. " + + "The service account needs to be configured in your identity provider." + oidcScopesHelp = "Flag to set the scopes which are expected from the OIDC request." ) parser := argparse.NewParser("otc-auth", "OTC-Auth Command Line Interface for managing OTC clouds.") @@ -62,29 +73,62 @@ func main() { // Login & common commands loginCommand := parser.NewCommand("login", "Login to the Open Telekom Cloud and receive an unscoped token.") - username = loginCommand.String("u", osUsername, &argparse.Options{Required: false, Help: fmt.Sprintf("Username for the OTC IAM system. %s %s", provideArgumentHelp, envOsUsername)}) - password = loginCommand.String("p", osPassword, &argparse.Options{Required: false, Help: fmt.Sprintf("Password for the OTC IAM system. %s %s", provideArgumentHelp, envOsPassword)}) - domainName = loginCommand.String("d", osDomainName, &argparse.Options{Required: false, Help: fmt.Sprintf("OTC domain name. %s %s", provideArgumentHelp, envOsDomainName)}) - overwriteToken = loginCommand.Flag("o", overwriteTokenArg, &argparse.Options{Required: false, Help: overwriteTokenHelp, Default: false}) + username = loginCommand.String( + "u", osUsername, + &argparse.Options{Required: false, Help: fmt.Sprintf( + "Username for the OTC IAM system. %s %s", provideArgumentHelp, envOsUsername)}) + password = loginCommand.String( + "p", osPassword, + &argparse.Options{Required: false, Help: fmt.Sprintf( + "Password for the OTC IAM system. %s %s", provideArgumentHelp, envOsPassword)}) + domainName = loginCommand.String( + "d", osDomainName, + &argparse.Options{ + Required: false, + Help: fmt.Sprintf("OTC domain name. %s %s", provideArgumentHelp, envOsDomainName), + }) + regionCode = loginCommand.String( + "r", region, &argparse.Options{ + Required: false, + Help: fmt.Sprintf("OTC region code. %s %s", provideArgumentHelp, envRegion), + }) + overwriteToken = loginCommand.Flag( + "o", overwriteTokenArg, &argparse.Options{Required: false, Help: overwriteTokenHelp, Default: false}) identityProvider = loginCommand.String("i", idpName, &argparse.Options{Required: false, Help: idpCommandHelp}) - identityProviderUrl = loginCommand.String("", idpUrlArg, &argparse.Options{Required: false, Help: idpUrlCommandHelp}) + identityProviderURL = loginCommand.String("", idpURLArg, &argparse.Options{Required: false, Help: idpURLCommandHelp}) // Remove Login information removeLoginCommand := loginCommand.NewCommand("remove", "Removes login information for a cloud") // Login with IAM - loginIamCommand := loginCommand.NewCommand("iam", "Login to the Open Telekom Cloud through its Identity and Access Management system.") - totp := loginIamCommand.String("t", totpArg, &argparse.Options{Required: false, Help: "6-digit time-based one-time password (TOTP) used for the MFA login flow."}) - userDomainId := loginIamCommand.String("", osUserDomainId, &argparse.Options{Required: false, Help: fmt.Sprintf("User Id number, can be obtained on the \"My Credentials page\" on the OTC. Required if --totp is provided. %s %s", provideArgumentHelp, envOsUserDomainId)}) + loginIamCommand := loginCommand.NewCommand( + "iam", "Login to the Open Telekom Cloud through its Identity and Access Management system.") + totp := loginIamCommand.String( + "t", totpArg, + &argparse.Options{Required: false, Help: "6-digit time-based one-time password (TOTP) used for the MFA login flow."}) + userDomainID := loginIamCommand.String( + "", osUserDomainID, + &argparse.Options{Required: false, Help: fmt.Sprintf( + "User ID number, can be obtained on the \"My Credentials page\" on the OTC. Required if --totp"+ + " is provided. %s %s", provideArgumentHelp, envOsUserDomainID)}) // Login with IDP + SAML - loginIdpSamlCommand := loginCommand.NewCommand("idp-saml", "Login to the Open Telekom Cloud through an Identity Provider and SAML.") + loginIdpSamlCommand := loginCommand.NewCommand( + "idp-saml", "Login to the Open Telekom Cloud through an Identity Provider and SAML.") // Login with IDP + OIDC - loginIdpOidcCommand := loginCommand.NewCommand("idp-oidc", "Login to the Open Telekom Cloud through an Identity Provider and OIDC.") - clientId := loginIdpOidcCommand.String("c", clientIdArg, &argparse.Options{Required: false, Help: fmt.Sprintf("Client Id as set on the IdP. %s %s", provideArgumentHelp, envClientId)}) - clientSecret := loginIdpOidcCommand.String("s", clientSecretArg, &argparse.Options{Required: false, Help: fmt.Sprintf("Secret Id as set on the IdP. %s %s", provideArgumentHelp, envClientSecret)}) - isServiceAccount = loginIdpOidcCommand.Flag("", isServiceAccountArg, &argparse.Options{Required: false, Help: isServiceAccountHelp}) + loginIdpOidcCommand := loginCommand.NewCommand( + "idp-oidc", "Login to the Open Telekom Cloud through an Identity Provider and OIDC.") + clientID := loginIdpOidcCommand.String( + "c", clientIDArg, + &argparse.Options{Required: false, Help: fmt.Sprintf("Client ID as set on the IdP. %s %s", + provideArgumentHelp, envClientID)}) + clientSecret := loginIdpOidcCommand.String( + "s", clientSecretArg, + &argparse.Options{Required: false, Help: fmt.Sprintf("Secret ID as set on the IdP. %s %s", + provideArgumentHelp, envClientSecret)}) + isServiceAccount = loginIdpOidcCommand.Flag( + "", isServiceAccountArg, &argparse.Options{Required: false, Help: isServiceAccountHelp}) oidcScopes := loginIdpOidcCommand.String("", oidcScopesArg, &argparse.Options{Required: false, Help: oidcScopesHelp}) // List Projects projectsCommand := parser.NewCommand("projects", "Manage Project Information") @@ -92,28 +136,56 @@ func main() { // Manage Cloud Container Engine cceCommand := parser.NewCommand("cce", "Manage Cloud Container Engine.") - projectName := cceCommand.String("p", osProjectName, &argparse.Options{Required: false, Help: fmt.Sprintf("Name of the project you want to access. %s %s.", provideArgumentHelp, envOsProjectName)}) - cceDomainName := cceCommand.String("d", osDomainName, &argparse.Options{Required: false, Help: fmt.Sprintf("OTC domain name. %s %s", provideArgumentHelp, envOsDomainName)}) + projectName := cceCommand.String( + "p", osProjectName, + &argparse.Options{Required: false, Help: fmt.Sprintf("Name of the project you want to access. %s %s.", + provideArgumentHelp, envOsProjectName)}) + cceDomainName := cceCommand.String( + "d", osDomainName, + &argparse.Options{Required: false, Help: fmt.Sprintf("OTC domain name. %s %s", provideArgumentHelp, envOsDomainName)}) // List clusters getClustersCommand := cceCommand.NewCommand("list", "Lists Project Clusters in CCE.") // Get Kubernetes Configuration - getKubeConfigCommand := cceCommand.NewCommand("get-kube-config", "Get remote kube config and merge it with existing local config file.") - clusterName := getKubeConfigCommand.String("c", clusterArg, &argparse.Options{Required: false, Help: fmt.Sprintf("Name of the clusterArg you want to access %s %s.", provideArgumentHelp, envClusterName)}) - daysValid := getKubeConfigCommand.String("v", "days-valid", &argparse.Options{Required: false, Help: "Period (in days) that the config will be valid", Default: "7"}) - targetLocation := getKubeConfigCommand.String("l", "target-location", &argparse.Options{Required: false, Help: "Where the kube config should be saved, Default: ~/.kube/config"}) + getKubeConfigCommand := cceCommand.NewCommand( + "get-kube-config", "Get remote kube config and merge it with existing local config file.") + clusterName := getKubeConfigCommand.String( + "c", clusterArg, &argparse.Options{ + Required: false, + Help: fmt.Sprintf("Name of the clusterArg you want to access %s %s.", provideArgumentHelp, envClusterName), + }) + daysValid := getKubeConfigCommand.String( + "v", "days-valid", + &argparse.Options{Required: false, Help: "Period (in days) that the config will be valid", Default: "7"}) + targetLocation := getKubeConfigCommand.String( + "l", "target-location", + &argparse.Options{Required: false, Help: "Where the kube config should be saved, Default: ~/.kube/config"}) // AK/SK Management accessTokenCommand := parser.NewCommand("access-token", "Manage AK/SK.") accessTokenCommandCreate := accessTokenCommand.NewCommand("create", "Create new AK/SK.") - atDomainName := accessTokenCommand.String("d", osDomainName, &argparse.Options{Required: false, Help: fmt.Sprintf("OTC domain name. %s %s", provideArgumentHelp, envOsDomainName)}) - durationSeconds := accessTokenCommandCreate.Int("t", "duration-seconds", &argparse.Options{Required: false, Help: "Lifetime of AK/SK, min 900 seconds.", Default: 900}) - - //Openstack Management + tokenDescription := accessTokenCommandCreate.String( + "s", "description", + &argparse.Options{Required: false, Help: "Description of the token.", Default: "Token by otc-auth"}) + accessTokenCommandList := accessTokenCommand.NewCommand("list", "List existing AK/SKs.") + accessTokenCommandDelete := accessTokenCommand.NewCommand("delete", "Delete existing AK/SK.") + token := accessTokenCommandDelete.String( + "t", "token", &argparse.Options{Required: true, Help: "The AK/SK token to delete."}) + atDomainName := accessTokenCommand.String("d", osDomainName, &argparse.Options{ + Required: false, + Help: fmt.Sprintf("OTC domain name. %s %s", provideArgumentHelp, envOsDomainName), + }) + + // Openstack Management openStackCommand := parser.NewCommand("openstack", "Manage Openstack Integration") openStackCommandCreateConfigFile := openStackCommand.NewCommand("config-create", "Creates new clouds.yaml") - openStackConfigLocation := openStackCommand.String("l", "config-location", &argparse.Options{Required: false, Help: "Where the config should be saved, Default: ~/.config/openstack/clouds.yaml"}) + openStackConfigLocation := openStackCommand.String( + "l", "config-location", + &argparse.Options{ + Required: false, + Help: "Where the config should be saved, Default: ~/.config/openstack/clouds.yaml", + }) err := parser.Parse(os.Args) if err != nil { @@ -121,21 +193,23 @@ func main() { } if versionCommand.Happened() { - _, err := fmt.Fprintf(os.Stdout, "OTC-Auth %s (%s)", version, date) - if err != nil { - common.OutputErrorToConsoleAndExit(err, "fatal: could not print tool version.") + _, errVersion := fmt.Fprintf(os.Stdout, "OTC-Auth %s (%s)", version, date) + if errVersion != nil { + common.OutputErrorToConsoleAndExit(errVersion, "fatal: could not print tool version.") } } if loginIamCommand.Happened() { - totpToken, userId := checkMFAFlowIAM(*totp, *userDomainId) + totpToken, userID := checkMFAFlowIAM(*totp, *userDomainID) + authInfo := common.AuthInfo{ AuthType: authTypeIAM, Username: getUsernameOrThrow(*username), Password: getPasswordOrThrow(*password), DomainName: getDomainNameOrThrow(*domainName), + Region: getRegionCodeOrThrow(*regionCode), Otp: totpToken, - UserDomainId: userId, + UserDomainID: userID, OverwriteFile: *overwriteToken, } @@ -143,14 +217,14 @@ func main() { } if loginIdpSamlCommand.Happened() { - identityProvider, identityProviderUrl := getIdpInfoOrThrow(*identityProvider, *identityProviderUrl) + identityProvider, identityProviderURL := getIdpInfoOrThrow(*identityProvider, *identityProviderURL) authInfo := common.AuthInfo{ AuthType: authTypeIDP, Username: getUsernameOrThrow(*username), Password: getPasswordOrThrow(*password), DomainName: getDomainNameOrThrow(*domainName), IdpName: identityProvider, - IdpUrl: identityProviderUrl, + IdpURL: identityProviderURL, AuthProtocol: protocolSAML, OverwriteFile: *overwriteToken, } @@ -159,18 +233,19 @@ func main() { } if loginIdpOidcCommand.Happened() { - identityProvider, identityProviderUrl := getIdpInfoOrThrow(*identityProvider, *identityProviderUrl) + identityProvider, identityProviderURL := getIdpInfoOrThrow(*identityProvider, *identityProviderURL) authInfo := common.AuthInfo{ AuthType: authTypeIDP, IdpName: identityProvider, - IdpUrl: identityProviderUrl, + IdpURL: identityProviderURL, AuthProtocol: protocolOIDC, DomainName: getDomainNameOrThrow(*domainName), - ClientId: getClientIdOrThrow(*clientId), + ClientID: getClientIDOrThrow(*clientID), ClientSecret: findClientSecretOrReturnEmpty(*clientSecret), OverwriteFile: *overwriteToken, IsServiceAccount: *isServiceAccount, OidcScopes: getOidcScopes(*oidcScopes), + Region: getRegionCodeOrThrow(*regionCode), } AuthenticateAndGetUnscopedToken(authInfo) @@ -190,7 +265,8 @@ func main() { config.LoadCloudConfig(domainName) if !config.IsAuthenticationValid() { - common.OutputErrorMessageToConsoleAndExit("fatal: no valid unscoped token found.\n\nPlease obtain an unscoped token by logging in first.") + common.OutputErrorMessageToConsoleAndExit( + "fatal: no valid unscoped token found.\n\nPlease obtain an unscoped token by logging in first.") } project := getProjectNameOrThrow(*projectName) @@ -220,17 +296,60 @@ func main() { config.LoadCloudConfig(domainName) if !config.IsAuthenticationValid() { - common.OutputErrorMessageToConsoleAndExit("fatal: no valid unscoped token found.\n\nPlease obtain an unscoped token by logging in first.") + common.OutputErrorMessageToConsoleAndExit( + "fatal: no valid unscoped token found.\n\nPlease obtain an unscoped token by logging in first.") + } + + accesstoken.CreateAccessToken(*tokenDescription) + } + + if accessTokenCommandList.Happened() { + domainName := getDomainNameOrThrow(*atDomainName) + config.LoadCloudConfig(domainName) + + if !config.IsAuthenticationValid() { + common.OutputErrorMessageToConsoleAndExit( + "fatal: no valid unscoped token found.\n\nPlease obtain an unscoped token by logging in first.") } - if *durationSeconds < 900 { - common.OutputErrorMessageToConsoleAndExit("fatal: argument duration-seconds may not be smaller then 900 seconds") + accessTokens, err2 := accesstoken.ListAccessToken() + if err2 != nil { + common.OutputErrorToConsoleAndExit(err2) + } + if len(accessTokens) > 0 { + log.Println("\nAccess Tokens:") + for _, aT := range accessTokens { + log.Printf("\nToken: \t\t%s\n"+ + "Description: \t%s\n"+ + "Created by: \t%s\n"+ + "Last Used: \t%s\n"+ + "Active: \t%s\n \n", + aT.AccessKey, aT.Description, aT.UserID, aT.LastUseTime, aT.Status) + } + } else { + log.Println("No access-tokens found") } - accesstoken.CreateAccessToken(*durationSeconds) } - if openStackCommandCreateConfigFile.Happened() { - openstack.WriteOpenStackCloudsYaml(*openStackConfigLocation) + if accessTokenCommandDelete.Happened() { + domainName := getDomainNameOrThrow(*atDomainName) + config.LoadCloudConfig(domainName) + + if !config.IsAuthenticationValid() { + common.OutputErrorMessageToConsoleAndExit( + "fatal: no valid unscoped token found.\n\nPlease obtain an unscoped token by logging in first.") + } + + if *token == "" { + common.OutputErrorMessageToConsoleAndExit("fatal: argument token cannot be empty.") + } + errDelete := accesstoken.DeleteAccessToken(*token) + if errDelete != nil { + common.OutputErrorToConsoleAndExit(errDelete) + } } + if openStackCommandCreateConfigFile.Happened() { + openstack.WriteOpenStackCloudsYaml(*openStackConfigLocation, getRegionCodeOrThrow(*regionCode)) + } } diff --git a/oidc/oidc.go b/oidc/oidc.go index d74eecc4..73ab0f32 100644 --- a/oidc/oidc.go +++ b/oidc/oidc.go @@ -2,10 +2,12 @@ package oidc import ( "fmt" + "io" "net/http" + "strings" + "otc-auth/common" "otc-auth/common/endpoints" - "strings" "github.com/go-http-utils/headers" ) @@ -21,8 +23,10 @@ func AuthenticateAndGetUnscopedToken(authInfo common.AuthInfo) common.TokenRespo return authenticateWithServiceProvider(oidcCredentials, authInfo) } -func authenticateWithServiceProvider(oidcCredentials common.OidcCredentialsResponse, authInfo common.AuthInfo) (tokenResponse common.TokenResponse) { - url := endpoints.IdentityProviders(authInfo.IdpName, authInfo.AuthProtocol) +//nolint:lll // This function will be removed soon +func authenticateWithServiceProvider(oidcCredentials common.OidcCredentialsResponse, authInfo common.AuthInfo) common.TokenResponse { + var tokenResponse common.TokenResponse + url := endpoints.IdentityProviders(authInfo.IdpName, authInfo.AuthProtocol, authInfo.Region) request := common.GetRequest(http.MethodPost, url, nil) @@ -33,10 +37,15 @@ func authenticateWithServiceProvider(oidcCredentials common.OidcCredentialsRespo headers.Authorization, oidcCredentials.BearerToken, ) - response := common.HttpClientMakeRequest(request) + response := common.HTTPClientMakeRequest(request) //nolint:bodyclose,lll // Works fine for now, this method will be replaced soon tokenResponse = common.GetCloudCredentialsFromResponseOrThrow(response) tokenResponse.Token.User.Name = oidcCredentials.Claims.PreferredUsername - defer response.Body.Close() - return + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + common.OutputErrorToConsoleAndExit(err) + } + }(response.Body) + return tokenResponse } diff --git a/oidc/server.go b/oidc/server.go index 9d0fb1a5..0305f5fa 100644 --- a/oidc/server.go +++ b/oidc/server.go @@ -3,18 +3,21 @@ package oidc import ( "context" "fmt" + "net/http" + "strings" + + "otc-auth/common" + "github.com/coreos/go-oidc/v3/oidc" "github.com/go-http-utils/headers" "github.com/google/uuid" "github.com/pkg/browser" "golang.org/x/oauth2" - "net/http" - "otc-auth/common" - "strings" ) +//nolint:gochecknoglobals // This file will be removed soon var ( - ctx = context.Background() + backgroundCtx = context.Background() oAuth2Config oauth2.Config state string @@ -30,26 +33,26 @@ const ( idTokenField = "id_token" ) -func startAndListenHttpServer(channel chan common.OidcCredentialsResponse) { - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - rawAccessToken := r.Header.Get(headers.Authorization) - if rawAccessToken == "" { - http.Redirect(w, r, oAuth2Config.AuthCodeURL(state), http.StatusFound) - return - } - - parts := strings.Split(rawAccessToken, " ") - if len(parts) != 2 { - w.WriteHeader(400) - return - } +func handleRoot(w http.ResponseWriter, r *http.Request) { + rawAccessToken := r.Header.Get(headers.Authorization) + if rawAccessToken == "" { + http.Redirect(w, r, oAuth2Config.AuthCodeURL(state), http.StatusFound) + return + } + parts := strings.Split(rawAccessToken, " ") + if len(parts) != 2 { //nolint:gomnd // Bearer tokens need to be of the format "Bearer ey..." + w.WriteHeader(http.StatusBadRequest) + return + } + _, err := idTokenVerifier.Verify(backgroundCtx, parts[1]) + if err != nil { + http.Redirect(w, r, oAuth2Config.AuthCodeURL(state), http.StatusFound) + return + } +} - _, err := idTokenVerifier.Verify(ctx, parts[1]) - if err != nil { - http.Redirect(w, r, oAuth2Config.AuthCodeURL(state), http.StatusFound) - return - } - }) +func startAndListenHTTPServer(channel chan common.OidcCredentialsResponse) { + http.HandleFunc("/", handleRoot) http.HandleFunc("/oidc/auth", func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get(queryState) != state { @@ -57,7 +60,7 @@ func startAndListenHttpServer(channel chan common.OidcCredentialsResponse) { return } - oauth2Token, err := oAuth2Config.Exchange(ctx, r.URL.Query().Get(queryCode)) + oauth2Token, err := oAuth2Config.Exchange(backgroundCtx, r.URL.Query().Get(queryCode)) if err != nil { http.Error(w, fmt.Sprintf("Failed to exchange token: %s", err.Error()), http.StatusInternalServerError) return @@ -68,19 +71,20 @@ func startAndListenHttpServer(channel chan common.OidcCredentialsResponse) { http.Error(w, "No id_token field in oauth2 token.", http.StatusInternalServerError) return } - rawIdToken, err := idTokenVerifier.Verify(ctx, idToken) + rawIDToken, err := idTokenVerifier.Verify(backgroundCtx, idToken) if err != nil { http.Error(w, fmt.Sprintf("Failed to verify ID Token: %s", err.Error()), http.StatusInternalServerError) return } oidcUsernameAndToken := common.OidcCredentialsResponse{} - if err := rawIdToken.Claims(&oidcUsernameAndToken.Claims); err != nil { + err = rawIDToken.Claims(&oidcUsernameAndToken.Claims) + if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } - _, err = w.Write([]byte(common.SuccessPageHtml)) + _, err = w.Write([]byte(common.SuccessPageHTML)) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -92,7 +96,7 @@ func startAndListenHttpServer(channel chan common.OidcCredentialsResponse) { } }) - err := http.ListenAndServe(localhost, nil) + err := http.ListenAndServe(localhost, nil) //nolint:gosec,lll // Complains about not being able to set timeouts, but this function will be removed soon anyway if err != nil { common.OutputErrorToConsoleAndExit(err, fmt.Sprintf("failed to start server at %s", localhost)) } @@ -100,22 +104,22 @@ func startAndListenHttpServer(channel chan common.OidcCredentialsResponse) { func authenticateWithIdp(params common.AuthInfo) common.OidcCredentialsResponse { channel := make(chan common.OidcCredentialsResponse) - go startAndListenHttpServer(channel) + go startAndListenHTTPServer(channel) ctx := context.Background() - provider, err := oidc.NewProvider(ctx, params.IdpUrl) + provider, err := oidc.NewProvider(ctx, params.IdpURL) if err != nil { common.OutputErrorToConsoleAndExit(err) } oAuth2Config = oauth2.Config{ - ClientID: params.ClientId, + ClientID: params.ClientID, ClientSecret: params.ClientSecret, RedirectURL: redirectURL, Endpoint: provider.Endpoint(), Scopes: params.OidcScopes, } - idTokenVerifier = provider.Verifier(&oidc.Config{ClientID: params.ClientId}) + idTokenVerifier = provider.Verifier(&oidc.Config{ClientID: params.ClientID}) state = uuid.New().String() err = browser.OpenURL(fmt.Sprintf("http://%s", localhost)) diff --git a/oidc/service_account.go b/oidc/service_account.go index a40d1a63..8abc12d5 100644 --- a/oidc/service_account.go +++ b/oidc/service_account.go @@ -2,19 +2,21 @@ package oidc import ( "encoding/json" - "github.com/go-http-utils/headers" "net/http" "net/url" - "otc-auth/common" "strings" + + "otc-auth/common" + + "github.com/go-http-utils/headers" ) -func createServiceAccountAuthenticateRequest(requestUrl string, clientId string, clientSecret string) *http.Request { +func createServiceAccountAuthenticateRequest(requestURL string, clientID string, clientSecret string) *http.Request { data := url.Values{} data.Set("grant_type", "client_credentials") data.Set("scope", "openid") - request := common.GetRequest(http.MethodPost, requestUrl, strings.NewReader(data.Encode())) - request.SetBasicAuth(clientId, clientSecret) + request := common.GetRequest(http.MethodPost, requestURL, strings.NewReader(data.Encode())) + request.SetBasicAuth(clientID, clientSecret) request.Header.Add(headers.ContentType, "application/x-www-form-urlencoded") return request } @@ -22,7 +24,7 @@ func createServiceAccountAuthenticateRequest(requestUrl string, clientId string, type ServiceAccountResponse struct { RefreshExpiresIn int `json:"refresh_expires_in"` TokenType string `json:"token_type"` - IdToken string `json:"id_token"` + IDToken string `json:"id_token"` NotBeforePolicy int `json:"not-before-policy"` SessionState string `json:"session_state"` AccessToken string `json:"access_token"` @@ -32,12 +34,12 @@ type ServiceAccountResponse struct { } func authenticateServiceAccountWithIdp(params common.AuthInfo) common.OidcCredentialsResponse { - idpTokenUrl, err := url.JoinPath(params.IdpUrl, "protocol/openid-connect/token") + idpTokenURL, err := url.JoinPath(params.IdpURL, "protocol/openid-connect/token") if err != nil { common.OutputErrorToConsoleAndExit(err) } - request := createServiceAccountAuthenticateRequest(idpTokenUrl, params.ClientId, params.ClientSecret) - response := common.HttpClientMakeRequest(request) + request := createServiceAccountAuthenticateRequest(idpTokenURL, params.ClientID, params.ClientSecret) + response := common.HTTPClientMakeRequest(request) //nolint:bodyclose,lll // Works fine for now, this method will be replaced soon bodyBytes := common.GetBodyBytesFromResponse(response) var result ServiceAccountResponse @@ -47,7 +49,7 @@ func authenticateServiceAccountWithIdp(params common.AuthInfo) common.OidcCreden } serviceAccountCreds := common.OidcCredentialsResponse{} - serviceAccountCreds.BearerToken = result.IdToken + serviceAccountCreds.BearerToken = result.IDToken serviceAccountCreds.Claims.PreferredUsername = "ServiceAccount" return serviceAccountCreds diff --git a/openstack/openstack.go b/openstack/openstack.go index 14e12958..b1f0fdfd 100644 --- a/openstack/openstack.go +++ b/openstack/openstack.go @@ -1,33 +1,37 @@ package openstack import ( - "github.com/gophercloud/utils/openstack/clientconfig" - "gopkg.in/yaml.v2" + "log" "os" + "path" + "path/filepath" + "otc-auth/common" "otc-auth/common/endpoints" "otc-auth/config" - "path" - "path/filepath" + + "github.com/gophercloud/utils/openstack/clientconfig" + "gopkg.in/yaml.v2" ) -func WriteOpenStackCloudsYaml(openStackConfigFileLocation string) { +func WriteOpenStackCloudsYaml(openStackConfigFileLocation string, regionCode string) { cloudConfig := config.GetActiveCloudConfig() domainName := cloudConfig.Domain.Name clouds := make(map[string]clientconfig.Cloud) for _, project := range cloudConfig.Projects { cloudName := domainName + "_" + project.Name - clouds[cloudName] = createOpenstackCloudConfig(project, domainName) + clouds[cloudName] = createOpenstackCloudConfig(project, domainName, regionCode) } + createOpenstackCloudsYAML(clientconfig.Clouds{Clouds: clouds}, openStackConfigFileLocation) } -func createOpenstackCloudConfig(project config.Project, domainName string) clientconfig.Cloud { +func createOpenstackCloudConfig(project config.Project, domainName string, regionCode string) clientconfig.Cloud { projectName := project.Name cloudName := domainName + "_" + projectName authInfo := clientconfig.AuthInfo{ - AuthURL: endpoints.BaseUrlIam + "/v3", + AuthURL: endpoints.BaseURLIam(regionCode) + "/v3", Token: project.ScopedToken.Secret, ProjectDomainName: projectName, } @@ -58,5 +62,5 @@ func createOpenstackCloudsYAML(clouds clientconfig.Clouds, openStackConfigFileLo } config.WriteConfigFile(string(contentAsBytes), openStackConfigFileLocation) - println("info: openstack clouds.yaml was updated") + log.Println("info: openstack clouds.yaml was updated") } diff --git a/saml/saml.go b/saml/saml.go index 2e41000e..348ed03b 100644 --- a/saml/saml.go +++ b/saml/saml.go @@ -3,51 +3,65 @@ package saml import ( "bytes" "encoding/xml" - "github.com/go-http-utils/headers" + "io" "net/http" + "otc-auth/common" "otc-auth/common/endpoints" "otc-auth/common/headervalues" header "otc-auth/common/xheaders" + + "github.com/go-http-utils/headers" ) func AuthenticateAndGetUnscopedToken(authInfo common.AuthInfo) (tokenResponse common.TokenResponse) { - spInitiatedRequest := getServiceProviderInitiatedRequest(authInfo) + spInitiatedRequest := getServiceProviderInitiatedRequest(authInfo) //nolint:bodyclose,lll // Works fine for now, this method will be replaced soon bodyBytes := authenticateWithIdp(authInfo, spInitiatedRequest) assertionResult := common.SamlAssertionResponse{} + err := xml.Unmarshal(bodyBytes, &assertionResult) if err != nil { common.OutputErrorToConsoleAndExit(err, "fatal: error deserializing xml.\ntrace: %s") } - response := validateAuthenticationWithServiceProvider(assertionResult, bodyBytes) + response := validateAuthenticationWithServiceProvider(assertionResult, bodyBytes) //nolint:bodyclose,lll // Works fine for now, this method will be replaced soon tokenResponse = common.GetCloudCredentialsFromResponseOrThrow(response) - defer response.Body.Close() - return + + defer func(Body io.ReadCloser) { + errClose := Body.Close() + if errClose != nil { + common.OutputErrorToConsoleAndExit(errClose) + } + }(response.Body) + + return tokenResponse } func getServiceProviderInitiatedRequest(params common.AuthInfo) *http.Response { - request := common.GetRequest(http.MethodGet, endpoints.IdentityProviders(params.IdpName, params.AuthProtocol), nil) + request := common.GetRequest(http.MethodGet, + endpoints.IdentityProviders(params.IdpName, params.AuthProtocol, params.Region), nil) request.Header.Add(headers.Accept, headervalues.ApplicationPaos) request.Header.Add(header.Paos, headervalues.Paos) - return common.HttpClientMakeRequest(request) + return common.HTTPClientMakeRequest(request) } func authenticateWithIdp(params common.AuthInfo, samlResponse *http.Response) []byte { - request := common.GetRequest(http.MethodPost, params.IdpUrl, samlResponse.Body) - request.Header.Add(headers.ContentType, headervalues.TextXml) + request := common.GetRequest(http.MethodPost, params.IdpURL, samlResponse.Body) + request.Header.Add(headers.ContentType, headervalues.TextXML) request.SetBasicAuth(params.Username, params.Password) - response := common.HttpClientMakeRequest(request) + response := common.HTTPClientMakeRequest(request) //nolint:bodyclose,lll // Works fine for now, this method will be replaced soon return common.GetBodyBytesFromResponse(response) } +//nolint:lll // This function will be removed soon func validateAuthenticationWithServiceProvider(assertionResult common.SamlAssertionResponse, responseBodyBytes []byte) *http.Response { - request := common.GetRequest(http.MethodPost, assertionResult.Header.Response.AssertionConsumerServiceURL, bytes.NewReader(responseBodyBytes)) + request := common.GetRequest(http.MethodPost, assertionResult.Header.Response.AssertionConsumerServiceURL, + bytes.NewReader(responseBodyBytes)) request.Header.Add(headers.ContentType, headervalues.ApplicationPaos) - return common.HttpClientMakeRequest(request) + return common.HTTPClientMakeRequest(request) } diff --git a/test/config_test.go b/test_test/config_test.go similarity index 82% rename from test/config_test.go rename to test_test/config_test.go index f8514be2..8af16819 100644 --- a/test/config_test.go +++ b/test_test/config_test.go @@ -1,24 +1,23 @@ -package test +package test_test import ( - "otc-auth/config" "testing" + + "otc-auth/config" ) -func TestLoadCloudConfig_init(t *testing.T) { - domainName := "first" +const firstDomain = "firstDomain" - config.LoadCloudConfig(domainName) +func TestLoadCloudConfig_init(t *testing.T) { + config.LoadCloudConfig(firstDomain) result := config.GetActiveCloudConfig().Domain - if result.Name != domainName { - t.Errorf("Expected result to contain cloud: %s, but result contains: %s ", domainName, result.Name) + if result.Name != firstDomain { + t.Errorf("Expected result to contain cloud: %s, but result contains: %s ", firstDomain, result.Name) } - } func TestLoadCloudConfig_two_domains(t *testing.T) { - firstDomain := "first" secondDomain := "second" config.LoadCloudConfig(firstDomain) @@ -31,8 +30,6 @@ func TestLoadCloudConfig_two_domains(t *testing.T) { } func TestLoadCloudConfig_make_domain_twice_active(t *testing.T) { - firstDomain := "first" - config.LoadCloudConfig(firstDomain) config.LoadCloudConfig(firstDomain) diff --git a/test/deserialization_test.go b/test_test/deserialization_test.go similarity index 86% rename from test/deserialization_test.go rename to test_test/deserialization_test.go index 7238ea7b..05de9672 100644 --- a/test/deserialization_test.go +++ b/test_test/deserialization_test.go @@ -1,19 +1,21 @@ -package test +package test_test import ( - "otc-auth/common" - "otc-auth/config" "reflect" "testing" + + "otc-auth/common" + "otc-auth/config" ) func TestDeserializeJsonForType__TokenResponse(t *testing.T) { desc := "token response gets deserialized" expected := getExpectedTokenResponse() var actual *common.TokenResponse + //nolint:lll input := []byte(`{"token":{"expires_at":"2022-11-30T14:01:54.956000Z","methods":["password"],"catalog":[{"endpoints":[{"id":"endpoint-id","interface":"endpoint-interface","region":"endpoint-region","region_id":"endpoint-region-id","url":"endpoint-url"}],"id":"catalog-id","name":"catalog-name","type":"catalog-type"}],"domain":{"id":"domain-id","name":"domain-name","xdomain_id":"x-domain-id","xdomain_type":"x-domain-type"},"roles":[{"id":"role-id","name":"role-name"}],"issued_at":"2022-11-29T14:01:54.956000Z","user":{"domain":{"id":"domain-id","name":"domain-name","xdomain_id":"x-domain-id","xdomain_type":"x-domain-type"},"id":"user-id","name":"user-name","password_expires_at":"2023-02-26T13:59:21.000000Z"}}}`) - actual = common.DeserializeJsonForType[common.TokenResponse](input) + actual = common.DeserializeJSONForType[common.TokenResponse](input) if !reflect.DeepEqual(expected, *actual) { t.Errorf("(%s): expected %s, actual %s", desc, expected, *actual) } @@ -22,10 +24,10 @@ func TestDeserializeJsonForType__TokenResponse(t *testing.T) { func TestDeserializeJsonForType__Cluster(t *testing.T) { var actual *config.Cluster desc := "cluster gets deserialized" - expected := config.Cluster{Name: "cluster-name", Id: "cluster-id"} + expected := config.Cluster{Name: "cluster-name", ID: "cluster-id"} input := []byte(`{"name":"cluster-name","id":"cluster-id"}`) - actual = common.DeserializeJsonForType[config.Cluster](input) + actual = common.DeserializeJSONForType[config.Cluster](input) if !reflect.DeepEqual(expected, *actual) { t.Errorf("(%s): expected %s, actual %s", desc, expected, *actual) @@ -36,7 +38,7 @@ func getExpectedTokenResponse() common.TokenResponse { expected := common.TokenResponse{} expected.Token.ExpiresAt = "2022-11-30T14:01:54.956000Z" expected.Token.IssuedAt = "2022-11-29T14:01:54.956000Z" - expected.Token.User.Domain.Id = "domain-id" + expected.Token.User.Domain.ID = "domain-id" expected.Token.User.Domain.Name = "domain-name" expected.Token.User.Name = "user-name" return expected diff --git a/test/model_test.go b/test_test/model_test.go similarity index 52% rename from test/model_test.go rename to test_test/model_test.go index facf518e..10274da1 100644 --- a/test/model_test.go +++ b/test_test/model_test.go @@ -1,10 +1,11 @@ -package test +package test_test import ( - "otc-auth/config" "reflect" "strings" "testing" + + "otc-auth/config" ) func TestCloudsSlice_RemoveCloudByNameIfExists(t *testing.T) { @@ -17,27 +18,27 @@ func TestCloudsSlice_RemoveCloudByNameIfExists(t *testing.T) { { desc: "cloud to be removed exists", actual: config.Clouds{ - {Domain: config.NameAndIdResource{Name: "cloud-1"}}, - {Domain: config.NameAndIdResource{Name: "cloud-2"}}, - {Domain: config.NameAndIdResource{Name: "cloud-3"}}, + {Domain: config.NameAndIDResource{Name: "cloud-1"}}, + {Domain: config.NameAndIDResource{Name: "cloud-2"}}, + {Domain: config.NameAndIDResource{Name: "cloud-3"}}, }, expected: config.Clouds{ - {Domain: config.NameAndIdResource{Name: "cloud-1"}}, - {Domain: config.NameAndIdResource{Name: "cloud-3"}}, + {Domain: config.NameAndIDResource{Name: "cloud-1"}}, + {Domain: config.NameAndIDResource{Name: "cloud-3"}}, }, input: "cloud-2", }, { desc: "cloud to be removed does not exist", actual: config.Clouds{ - {Domain: config.NameAndIdResource{Name: "cloud-1"}}, - {Domain: config.NameAndIdResource{Name: "cloud-2"}}, - {Domain: config.NameAndIdResource{Name: "cloud-3"}}, + {Domain: config.NameAndIDResource{Name: "cloud-1"}}, + {Domain: config.NameAndIDResource{Name: "cloud-2"}}, + {Domain: config.NameAndIDResource{Name: "cloud-3"}}, }, expected: config.Clouds{ - {Domain: config.NameAndIdResource{Name: "cloud-1"}}, - {Domain: config.NameAndIdResource{Name: "cloud-2"}}, - {Domain: config.NameAndIdResource{Name: "cloud-3"}}, + {Domain: config.NameAndIDResource{Name: "cloud-1"}}, + {Domain: config.NameAndIDResource{Name: "cloud-2"}}, + {Domain: config.NameAndIDResource{Name: "cloud-3"}}, }, input: "cloud-4", }, @@ -62,7 +63,10 @@ func TestCloudsSlice_RemoveCloudByNameIfExists(t *testing.T) { } if !reflect.DeepEqual(actualCloudNames, expectedCloudNames) { - t.Errorf("(%s): actual %s, expected %s", tc.desc, strings.Join(actualCloudNames, ", "), strings.Join(expectedCloudNames, ", ")) + t.Errorf("(%s): actual %s, expected %s", + tc.desc, + strings.Join(actualCloudNames, ", "), + strings.Join(expectedCloudNames, ", ")) } }) } @@ -78,28 +82,28 @@ func TestCloudsSlice_SetActiveByName(t *testing.T) { { desc: "set active makes all others inactive", expected: config.Clouds{ - {Domain: config.NameAndIdResource{Name: "cloud-1"}, Active: false}, - {Domain: config.NameAndIdResource{Name: "cloud-2"}, Active: true}, - {Domain: config.NameAndIdResource{Name: "cloud-3"}, Active: false}, + {Domain: config.NameAndIDResource{Name: "cloud-1"}, Active: false}, + {Domain: config.NameAndIDResource{Name: "cloud-2"}, Active: true}, + {Domain: config.NameAndIDResource{Name: "cloud-3"}, Active: false}, }, actual: config.Clouds{ - {Domain: config.NameAndIdResource{Name: "cloud-1"}, Active: true}, - {Domain: config.NameAndIdResource{Name: "cloud-2"}, Active: true}, - {Domain: config.NameAndIdResource{Name: "cloud-3"}, Active: true}, + {Domain: config.NameAndIDResource{Name: "cloud-1"}, Active: true}, + {Domain: config.NameAndIDResource{Name: "cloud-2"}, Active: true}, + {Domain: config.NameAndIDResource{Name: "cloud-3"}, Active: true}, }, input: "cloud-2", }, { desc: "set active with unknown name sets all inactive", expected: config.Clouds{ - {Domain: config.NameAndIdResource{Name: "cloud-1"}, Active: false}, - {Domain: config.NameAndIdResource{Name: "cloud-2"}, Active: false}, - {Domain: config.NameAndIdResource{Name: "cloud-3"}, Active: false}, + {Domain: config.NameAndIDResource{Name: "cloud-1"}, Active: false}, + {Domain: config.NameAndIDResource{Name: "cloud-2"}, Active: false}, + {Domain: config.NameAndIDResource{Name: "cloud-3"}, Active: false}, }, actual: config.Clouds{ - {Domain: config.NameAndIdResource{Name: "cloud-1"}, Active: true}, - {Domain: config.NameAndIdResource{Name: "cloud-2"}, Active: true}, - {Domain: config.NameAndIdResource{Name: "cloud-3"}, Active: true}, + {Domain: config.NameAndIDResource{Name: "cloud-1"}, Active: true}, + {Domain: config.NameAndIDResource{Name: "cloud-2"}, Active: true}, + {Domain: config.NameAndIDResource{Name: "cloud-3"}, Active: true}, }, input: "cloud-4", }, @@ -108,7 +112,9 @@ func TestCloudsSlice_SetActiveByName(t *testing.T) { t.Run(tc.desc, func(t *testing.T) { tc.actual.SetActiveByName(tc.input) if tc.actual.NumberOfActiveCloudConfigs() != tc.expected.NumberOfActiveCloudConfigs() { - t.Errorf("(%s): expected %d, actual %d", tc.desc, tc.expected.NumberOfActiveCloudConfigs(), tc.actual.NumberOfActiveCloudConfigs()) + t.Errorf("(%s): expected %d, actual %d", + tc.desc, tc.expected.NumberOfActiveCloudConfigs(), + tc.actual.NumberOfActiveCloudConfigs()) } }) }