pax_global_header00006660000000000000000000000064150267736650014533gustar00rootroot0000000000000052 comment=4d1c12e91714a48f1a6985702a052770e2477cf9 go-ovh-1.9.0/000077500000000000000000000000001502677366500127415ustar00rootroot00000000000000go-ovh-1.9.0/.github/000077500000000000000000000000001502677366500143015ustar00rootroot00000000000000go-ovh-1.9.0/.github/workflows/000077500000000000000000000000001502677366500163365ustar00rootroot00000000000000go-ovh-1.9.0/.github/workflows/golang-build.yaml000066400000000000000000000010771502677366500215730ustar00rootroot00000000000000name: Golang Build on: [push, workflow_dispatch, pull_request] jobs: build: runs-on: ubuntu-latest strategy: matrix: go-version: ["1.20", "1.21", "1.22", "1.23", "1.24"] steps: - uses: actions/checkout@v3 - name: Setup Go ${{ matrix.go-version }} uses: actions/setup-go@v3 with: go-version: ${{ matrix.go-version }} - name: Building run: go build -v ./... - name: Running golangci-lint uses: golangci/golangci-lint-action@v3 - name: Testing run: go test -v ./... go-ovh-1.9.0/.gitignore000066400000000000000000000000401502677366500147230ustar00rootroot00000000000000# Temporary edit files *.swp *~ go-ovh-1.9.0/CODEOWNERS000066400000000000000000000000541502677366500143330ustar00rootroot00000000000000* @ovh/su-developer-platform-api-exposition go-ovh-1.9.0/CONTRIBUTING.md000066400000000000000000000067401502677366500152010ustar00rootroot00000000000000# Contributing to go-ovh ## Submitting Modifications: So you want to contribute you work? Awesome! We are eager to review it. To submit your contribution, you must use Github Pull Requests. Your work does not need to be fully polished before submiting it. Actully, we love helping people writing a great contribution. Hence, if you are wondering how to integrate a specific change, feel free to start a discussion in a Pull Request. Before we can actually accept and merge a Pull Request, it will need to follow the conding guidelines (see below), and each commit shall be signed to indicate your full agreement with these guidelines and the DCO (see below). To sign a commit, you may use a command like: ``` # New commit git commit -s # Previous commit git commit --amend -s ``` If a Pull Request can not be automatically merged, you will probably need to "rebase" your work on latest project update: ``` # Assuming, this project remote is registered as "upstream" git fetch upstream git rebase upstream/master ``` ## Contribution guidelines 1. your code must follow the coding style rules (see below) 2. your code must be documented 3. you code must be tested 4. your work must be signed (see "Developer Certificate of Origin" below) 5. you may contribute through GitHub Pull Requests ## Coding and documentation Style: - Code must be formated with `gofmt -sw ./` - Code must pass `go vet ./...` - Code must pass `golint ./...` ## Licensing for new files: go-ovh is licensed under a (modified) BSD license. Anything contributed to go-ovh must be released under this license. When introducing a new file into the project, please make sure it has a copyright header making clear under which license it''s being released. ## Developer Certificate of Origin: ``` To improve tracking of contributions to this project we will use a process modeled on the modified DCO 1.1 and use a "sign-off" procedure on patches that are being contributed. The sign-off is a simple line at the end of the explanation for the patch, which certifies that you wrote it or otherwise have the right to pass it on as an open-source patch. The rules are pretty simple: if you can certify the below: By making a contribution to this project, I certify that: (a) The contribution was created in whole or in part by me and I have the right to submit it under the open source license indicated in the file; or (b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source License and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I am permitted to submit under a different license), as indicated in the file; or (c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it. (d) The contribution is made free of any other party''s intellectual property claims or rights. (e) I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved. then you just add a line saying Signed-off-by: Random J Developer using your real name (sorry, no pseudonyms or anonymous contributions.) ``` go-ovh-1.9.0/LICENSE000066400000000000000000000027331502677366500137530ustar00rootroot00000000000000BSD 3-Clause License Copyright (c) 2015-2025, OVH SAS Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. go-ovh-1.9.0/README.md000066400000000000000000000443421502677366500142270ustar00rootroot00000000000000go-ovh ====== Lightweight Go wrapper around OVHcloud's APIs. Handles all the hard work including credential creation and requests signing. [![GoDoc](https://godoc.org/github.com/ovh/go-ovh/go-ovh?status.svg)](http://godoc.org/github.com/ovh/go-ovh/ovh) [![Build Status](https://github.com/ovh/go-ovh/actions/workflows/golang-build.yaml/badge.svg?branch=master)](https://github.com/ovh/go-ovh/actions?query=workflow:golang-build) [![Coverage Status](https://coveralls.io/repos/github/ovh/go-ovh/badge.svg?branch=master)](https://coveralls.io/github/ovh/go-ovh?branch=master) [![Go Report Card](https://goreportcard.com/badge/ovh/go-ovh)](http://goreportcard.com/report/ovh/go-ovh) ```go package main import ( "fmt" "github.com/ovh/go-ovh/ovh" ) // PartialMe holds the first name of the currently logged-in user. // Visit https://api.ovh.com/console/#/me#GET for the full definition type PartialMe struct { Firstname string `json:"firstname"` } // Instantiate an OVH client and get the firstname of the currently logged-in user. // Visit https://api.ovh.com/createToken/index.cgi?GET=/me to get your credentials. func main() { var me PartialMe client, err := ovh.NewClient( "ovh-eu", YOUR_APPLICATION_KEY, YOUR_APPLICATION_SECRET, YOUR_CONSUMER_KEY, ) if err != nil { fmt.Printf("Error: %q\n", err) return } client.Get("/me", &me) fmt.Printf("Welcome %s!\n", me.Firstname) } ``` ## Installation The Golang wrapper has been tested with Golang 1.18+. It may worker with older versions although it has not been tested. To use it, just include it to your ``import`` and run ``go get``: ```go import ( ... "github.com/ovh/go-ovh/ovh" ) ``` ## Configuration The straightforward way to use OVHcloud's API keys is to embed them directly in the application code. While this is very convenient, it lacks of elegance and flexibility. Alternatively it is suggested to use configuration files or environment variables so that the same code may run seamlessly in multiple environments. Production and development for instance. `go-ovh` supports two forms of authentication: - OAuth2, using scopped service accounts, and compatible with OVHcloud IAM - application key & application secret & consumer key ### OAuth2 First, you need to generate a pair of valid `client_id` and `client_secret`: you can proceed by [following this documentation](https://help.ovhcloud.com/csm/en-manage-service-account?id=kb_article_view&sysparm_article=KB0059343) Once you have retrieved your `client_id` and `client_secret`, you can create and edit a configuration file that will be used by `go-ovh`. ```ini [default] ; general configuration: default endpoint endpoint=ovh-eu [ovh-eu] ; configuration specific to 'ovh-eu' endpoint client_id=my_client_id client_secret=my_client_secret ``` The client will successively attempt to locate this configuration file in 1. Current working directory: ``./ovh.conf`` 2. Current user's home directory: ``~/.ovh.conf`` 3. System wide configuration: ``/etc/ovh.conf`` Depending on the API you want to use, you may set the ``endpoint`` to: * ``ovh-eu`` for OVHcloud Europe API * ``ovh-us`` for OVHcloud US API * ``ovh-ca`` for OVHcloud Canada API This lookup mechanism makes it easy to overload credentials for a specific project or user. ### Access Token This authentication method is useful when short-lived credentials are necessary. E.g. oauth2 [plugin](https://github.com/puppetlabs/vault-plugin-secrets-oauthapp) for HashiCorp Vault can request an access token that would be used by OVHcloud terraform provider. Although this token, requested via data-source, would end up stored in the Terraform state file, that would pose less risk since the token validity would last for only 1 hour. Other applications are of course also possible. In order to use the access token with this wrapper either use `ovh.NewAccessTokenClient` to create the client, or pass the token via `OVH_ACCESS_TOKEN` environment variable to `ovh.NewDefaultClient`. ### Application Key/Application Secret If you have completed successfully the __OAuth2__ part, you can continue to [the Use the Lib part](https://github.com/ovh/go-ovh?tab=readme-ov-file#use-the-lib). This section will cover the legacy authentication method using application key and application secret. This wrapper will first look for direct instanciation parameters then ``OVH_ENDPOINT``, ``OVH_APPLICATION_KEY``, ``OVH_APPLICATION_SECRET`` and ``OVH_CONSUMER_KEY`` environment variables. If either of these parameter is not provided, it will look for a configuration file of the form: ```ini [default] ; general configuration: default endpoint endpoint=ovh-eu [ovh-eu] ; configuration specific to 'ovh-eu' endpoint application_key=my_app_key application_secret=my_application_secret consumer_key=my_consumer_key ``` Depending on the API you want to use, you may set the ``endpoint`` to: * ``ovh-eu`` for OVHcloud Europe API * ``ovh-us`` for OVHcloud US API * ``ovh-ca`` for OVHcloud Canada API * ``soyoustart-eu`` for So you Start Europe API * ``soyoustart-ca`` for So you Start Canada API * ``kimsufi-eu`` for Kimsufi Europe API * ``kimsufi-ca`` for Kimsufi Canada API * Or any arbitrary URL to use in a test for example The client will successively attempt to locate this configuration file in 1. Current working directory: ``./ovh.conf`` 2. Current user's home directory ``~/.ovh.conf`` 3. System wide configuration ``/etc/ovh.conf`` This lookup mechanism makes it easy to overload credentials for a specific project or user. #### Register your app OVHcloud's API, like most modern APIs is designed to authenticate both an application and a user, without requiring the user to provide a password. Your application will be identified by its "application secret" and "application key" tokens. Hence, to use the API, you must first register your application and then ask your user to authenticate on a specific URL. Once authenticated, you'll have a valid "consumer key" which will grant your application on specific APIs. The user may choose the validity period of his authorization. The default period is 24h. He may also revoke an authorization at any time. Hence, your application should be prepared to receive 403 HTTP errors and prompt the user to re-authenticate. This process is detailed in the following section. Alternatively, you may only need to build an application for a single user. In this case you may generate all credentials at once. See below. ##### Use the API on behalf of a user Visit [https://eu.api.ovh.com/createApp](https://eu.api.ovh.com/createApp) and create your app You'll get an application key and an application secret. To use the API you'll need a consumer key. The consumer key has two types of restriction: * path: eg. only the ```GET``` method on ```/me``` * time: eg. expire in 1 day Then, get a consumer key. Here's an example on how to generate one. First, create a 'ovh.conf' file in the current directory with the application key and application secret. You can add the consumer key once generated. For alternate configuration method, please see the [configuration section](#configuration). ```ini [ovh-eu] application_key=my_app_key application_secret=my_application_secret ; consumer_key=my_consumer_key ``` Then, you may use a program like this example to create a consumer key for the application: ```go package main import ( "fmt" "github.com/ovh/go-ovh/ovh" ) func main() { // Create a client using credentials from config files or environment variables client, err := ovh.NewEndpointClient("ovh-eu") if err != nil { fmt.Printf("Error: %q\n", err) return } ckReq := client.NewCkRequest() // Allow GET method on /me ckReq.AddRules(ovh.ReadOnly, "/me") // Allow GET method on /xdsl and all its sub routes ckReq.AddRecursiveRules(ovh.ReadOnly, "/xdsl") // Run the request response, err := ckReq.Do() if err != nil { fmt.Printf("Error: %q\n", err) return } // Print the validation URL and the Consumer key fmt.Printf("Generated consumer key: %s\n", response.ConsumerKey) fmt.Printf("Please visit %s to validate it\n", response.ValidationURL) } ``` ##### Use the API for a single user Alternatively, you may generate all creadentials at once, including the consumer key. You will typically want to do this when writing automation scripts for a single projects. If this case, you may want to directly go to https://eu.api.ovh.com/createToken/ to generate the 3 tokens at once. Make sure to save them in one of the 'ovh.conf' configuration file. Please see the [configuration section](#configuration). ``ovh.conf`` should look like: ```ini [ovh-eu] application_key=my_app_key application_secret=my_application_secret consumer_key=my_consumer_key ``` ## Use the lib These examples assume valid credentials are available in the [configuration](#configuration). ### GET ```go package main import ( "fmt" "github.com/ovh/go-ovh/ovh" ) func main() { client, err := ovh.NewEndpointClient("ovh-eu") if err != nil { fmt.Printf("Error: %q\n", err) return } // Get all the xdsl services xdslServices := []string{} if err := client.Get("/xdsl/", &xdslServices); err != nil { fmt.Printf("Error: %q\n", err) return } // xdslAccess represents a xdsl access returned by the API type xdslAccess struct { Name string `json:"accessName"` Status string `json:"status"` Pairs int `json:"pairsNumber"` // Insert the other properties here } // Get the details of each service for i, serviceName := range xdslServices { access := xdslAccess{} url := "/xdsl/" + serviceName if err := client.Get(url, &access); err != nil { fmt.Printf("Error: %q\n", err) return } fmt.Printf("#%d : %+v\n", i+1, access) } } ``` ### PUT ```go package main import ( "fmt" "github.com/ovh/go-ovh/ovh" ) func main() { client, err := ovh.NewEndpointClient("ovh-eu") if err != nil { fmt.Printf("Error: %q\n", err) return } // Params type AccessPutParams struct { Description string `json:"description"` } // Update the description of the service params := &AccessPutParams{Description: "My awesome access"} if err := client.Put("/xdsl/xdsl-yourservice", params, nil); err != nil { fmt.Printf("Error: %q\n", err) return } fmt.Println("Description updated") } ``` ### Use v1 and v2 API versions When using OVHcloud APIs (not So you Start or Kimsufi ones), you are given the opportunity to aim for two API versions. For the European API, for example: - the v1 is reachable through https://eu.api.ovh.com/v1 - the v2 is reachable through https://eu.api.ovh.com/v2 - the legacy URL is https://eu.api.ovh.com/1.0 Calling `client.Get`, you can target the API version you want: ```go client, _ := ovh.NewEndpointClient("ovh-eu") // Call to https://eu.api.ovh.com/v1/xdsl/xdsl-yourservice client.Get("/v1/xdsl/xdsl-yourservice", nil) // Call to https://eu.api.ovh.com/v2/xdsl/xdsl-yourservice client.Get("/v2/xdsl/xdsl-yourservice", nil) // Legacy call to https://eu.api.ovh.com/1.0/xdsl/xdsl-yourservice client.Get("/xdsl/xdsl-yourservice", nil) ``` ## API Documentation ### Create a client - Use ``ovh.NewDefaultClient()`` to create a client using endpoint and credentials from config files or environment - Use ``ovh.NewEndpointClient()`` to create a client for a specific API and use credentials from config files or environment - Use ``ovh.NewOAuth2Client()`` to have full control over their authentication, using OAuth2 authentication method - Use ``ovh.NewAccessTokenClient()`` to have full control over their authentication, using token that was previously issued by auth/oauth2/token endpoint - Use ``ovh.NewClient()`` to have full control over their authentication, using legacy authentication method ### Query Each HTTP verb has its own Client method. Some API methods supports unauthenticated calls. For these methods, you may want to use the ``*UnAuth`` variant of the Client which will bypass request signature. Each helper accepts a ``method`` and ``resType`` argument. ``method`` is the full URI, including the query string, and ``resType`` is a reference to an object in which the json response will be unserialized. Additionally, ``Post``, ``Put`` and their ``UnAuth`` variant accept a reqBody which is a reference to a json serializable object or nil. Alternatively, you may directly use the low level ``CallAPI`` method. - Use ``client.Get()`` for GET requests - Use ``client.Post()`` for POST requests - Use ``client.Put()`` for PUT requests - Use ``client.Delete()`` for DELETE requests Or, for unauthenticated requests: - Use ``client.GetUnAuth()`` for GET requests - Use ``client.PostUnAuth()`` for POST requests - Use ``client.PutUnAuth()`` for PUT requests - Use ``client.DeleteUnAuth()`` for DELETE requests ### Request consumer keys __[Only valid for legacy authentication method]__ Consumer keys may be restricted to a subset of the API. This allows to delegate the API to manage only a specific server or domain name for example. This is called "scoping" a consumer key. Rules are simple. They combine an HTTP verb (GET, POST, PUT or DELETE) with a pattern. A pattern is a plain API method and may contain the '*' wilcard to match "anything". Just like glob on a Unix machine. While this is simple and may be managed directly with the API as-is, this can be cumbersome to do and we recommend using the ``CkRequest`` helper. It basically manages the list of authorizations for you and the actual request. *example*: Grant on all /sms and identity ```go client, err := ovh.NewEndpointClient("ovh-eu") if err == nil { // Do something } req := client.NewCkRequest() req.AddRules(ovh.ReadOnly, "/me") req.AddRecursiveRulesRules(ovh.ReadWrite, "/sms") pendingCk, err := req.Do() ``` This example will generate a request for: - GET /me - GET /sms - GET /sms/* - POST /sms - POST /sms/* - PUT /sms - PUT /sms/* - DELETE /sms - DELETE /sms/* Which would be tedious to do by hand... *Create a ``CkRequest``*: ```go req := client.NewCkRequest() ``` *Request access on a specific path and method* (advanced): ```go // Use this method for fine-grain access control. In most case, you'll // want to use the methods below. req.AddRule("VERB", "PATTERN") ``` *Request access on specific path*: ```go // This will generate all patterns for GET PATH req.AddRules(ovh.ReadOnly, "/PATH") // This will generate all patterns for PATH for all HTTP verbs req.AddRules(ovh.ReadWrite, "/PATH") // This will generate all patterns for PATH for all HTTP verbs, except DELETE req.AddRules(ovh.ReadWriteSafe, "/PATH") ``` *Request access on path and all sub-path*: ```go // This will generate all patterns for GET PATH req.AddRecursiveRules(ovh.ReadOnly, "/PATH") // This will generate all patterns for PATH for all HTTP verbs req.AddRecursiveRules(ovh.ReadWrite, "/PATH") // This will generate all patterns for PATH for all HTTP verbs, except DELETE req.AddRecusriveRules(ovh.ReadWriteSafe, "/PATH") ``` *Create key*: ```go pendingCk, err := req.Do() ``` This will initiate the consumer key validation process and return both a consumer key and a validation URL. The consumer key is automatically added to the client which was used to create the request. It may be used as soon as the user has authenticated the request on the validation URL. ``pendingCk`` contains 3 fields: - ``ValidationURL`` the URL the user needs to visit to activate the consumer key - ``ConsumerKey`` the new consumer key. It won't be active until validation - ``State`` the consumer key state. Always "pendingValidation" at this stage ## Hacking This wrapper uses standard Go tools, so you should feel at home with it. Here is a quick outline of what it may look like. ### Get the sources ``` go get github.com/ovh/go-ovh/ovh cd $GOPATH/src/github.com/ovh/go-ovh/ovh go get ``` You've developed a new cool feature ? Fixed an annoying bug ? We'd be happy to hear from you ! See [CONTRIBUTING.md](https://github.com/ovh/go-ovh/blob/master/CONTRIBUTING.md) for more informations ### Run the tests Simply run ``go test``. Since we all love quality, please note that we do not accept contributions lowering coverage. ``` # Run all tests, with coverage go test -cover # Validate code quality golint ./... go vet ./... ``` ## Supported APIs ### OVHcloud Europe - **Documentation**: https://eu.api.ovh.com/ - **Community support**: api-subscribe@ml.ovh.net - **Console**: https://eu.api.ovh.com/console - **Create application credentials**: https://eu.api.ovh.com/createApp/ - **Create script credentials** (all keys at once): https://eu.api.ovh.com/createToken/ ### OVHcloud US - **Documentation**: https://api.us.ovhcloud.com/ - **Console**: https://api.us.ovhcloud.com/console/ - **Create application credentials**: https://api.us.ovhcloud.com/createApp/ - **Create script credentials** (all keys at once): https://api.us.ovhcloud.com/createToken/ ### OVHcloud Canada - **Documentation**: https://ca.api.ovh.com/ - **Community support**: api-subscribe@ml.ovh.net - **Console**: https://ca.api.ovh.com/console - **Create application credentials**: https://ca.api.ovh.com/createApp/ - **Create script credentials** (all keys at once): https://ca.api.ovh.com/createToken/ ### So you Start Europe - **Documentation**: https://eu.api.soyoustart.com/ - **Community support**: api-subscribe@ml.ovh.net - **Console**: https://eu.api.soyoustart.com/console/ - **Create application credentials**: https://eu.api.soyoustart.com/createApp/ - **Create script credentials** (all keys at once): https://eu.api.soyoustart.com/createToken/ ### So you Start Canada - **Documentation**: https://ca.api.soyoustart.com/ - **Community support**: api-subscribe@ml.ovh.net - **Console**: https://ca.api.soyoustart.com/console/ - **Create application credentials**: https://ca.api.soyoustart.com/createApp/ - **Create script credentials** (all keys at once): https://ca.api.soyoustart.com/createToken/ ### Kimsufi Europe - **Documentation**: https://eu.api.kimsufi.com/ - **Community support**: api-subscribe@ml.ovh.net - **Console**: https://eu.api.kimsufi.com/console/ - **Create application credentials**: https://eu.api.kimsufi.com/createApp/ - **Create script credentials** (all keys at once): https://eu.api.kimsufi.com/createToken/ ### Kimsufi Canada - **Documentation**: https://ca.api.kimsufi.com/ - **Community support**: api-subscribe@ml.ovh.net - **Console**: https://ca.api.kimsufi.com/console/ - **Create application credentials**: https://ca.api.kimsufi.com/createApp/ - **Create script credentials** (all keys at once): https://ca.api.kimsufi.com/createToken/ ## License 3-Clause BSD go-ovh-1.9.0/go.mod000066400000000000000000000011061502677366500140450ustar00rootroot00000000000000module github.com/ovh/go-ovh go 1.18 require ( github.com/jarcoal/httpmock v1.3.0 github.com/maxatome/go-testdeep v1.12.0 golang.org/x/oauth2 v0.18.0 gopkg.in/ini.v1 v1.67.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/stretchr/testify v1.8.2 // indirect golang.org/x/net v0.22.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.31.0 // indirect ) retract ( v1.4.1 // Configuration fetch from wrong folder v1.4.0 // Configuration fetch from wrong folder ) go-ovh-1.9.0/go.sum000066400000000000000000000076231502677366500141040ustar00rootroot00000000000000github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc= github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= go-ovh-1.9.0/ovh/000077500000000000000000000000001502677366500135355ustar00rootroot00000000000000go-ovh-1.9.0/ovh/configuration.go000066400000000000000000000140131502677366500167320ustar00rootroot00000000000000package ovh import ( "context" "errors" "fmt" "os" "os/user" "runtime" "strings" "golang.org/x/oauth2/clientcredentials" "gopkg.in/ini.v1" ) var configPaths = []string{ // System wide configuration "/etc/ovh.conf", // Configuration in user's home "~/.ovh.conf", // Configuration in local folder "./ovh.conf", } // currentUserHome attempts to get current user's home directory. func currentUserHome() (string, error) { usr, err := user.Current() if err != nil { // Fallback by trying to read $HOME if userHome := os.Getenv("HOME"); userHome != "" { return userHome, nil } return "", err } return usr.HomeDir, nil } // configPaths returns configPaths, with ~/ prefix expanded. func expandConfigPaths() []interface{} { paths := []interface{}{} // Will be initialized on first use var home string var homeErr error for _, path := range configPaths { if strings.HasPrefix(path, "~/") { // Find home if needed if home == "" && homeErr == nil { home, homeErr = currentUserHome() } // Ignore file in HOME if we cannot find it if homeErr != nil { continue } path = home + path[1:] } paths = append(paths, path) } return paths } // loadINI builds a ini.File from the configuration paths provided in configPaths. // It's a helper for loadConfig. func loadINI() (*ini.File, error) { // Don't try to load configuration from the // filesystem when compiling for WebAssembly if runtime.GOARCH == "wasm" && runtime.GOOS == "js" { return ini.Empty(), nil } paths := expandConfigPaths() if len(paths) == 0 { return ini.Empty(), nil } return ini.LooseLoad(paths[0], paths[1:]...) } // loadConfig loads client configuration from params, environments or configuration // files (by order of decreasing precedence). // // loadConfig will check OVH_CONSUMER_KEY, OVH_APPLICATION_KEY, OVH_APPLICATION_SECRET // and OVH_ENDPOINT environment variables. If any is present, it will take precedence // over any configuration from file. // // Configuration files are ini files. They share the same format as python-ovh, // node-ovh, php-ovh and all other wrappers. If any wrapper is configured, all // can re-use the same configuration. loadConfig will check for configuration in: // // - ./ovh.conf // - $HOME/.ovh.conf // - /etc/ovh.conf func (c *Client) loadConfig(endpointName string) error { if strings.HasSuffix(endpointName, "/") { return fmt.Errorf("endpoint name cannot have a trailing slash") } // Load configuration files by order of increasing priority. All configuration // files are optional. Only load file from user home if home could be resolve cfg, err := loadINI() if err != nil { return fmt.Errorf("cannot load configuration: %w", err) } // Canonicalize configuration if endpointName == "" { endpointName = getConfigValue(cfg, "default", "endpoint", "ovh-eu") } if c.AccessToken == "" { c.AccessToken = getConfigValue(cfg, endpointName, "access_token", "") } if c.AppKey == "" { c.AppKey = getConfigValue(cfg, endpointName, "application_key", "") } if c.AppSecret == "" { c.AppSecret = getConfigValue(cfg, endpointName, "application_secret", "") } if c.ConsumerKey == "" { c.ConsumerKey = getConfigValue(cfg, endpointName, "consumer_key", "") } if c.ClientID == "" { c.ClientID = getConfigValue(cfg, endpointName, "client_id", "") } if c.ClientSecret == "" { c.ClientSecret = getConfigValue(cfg, endpointName, "client_secret", "") } configuredAuthMethods := []string{} if c.AppKey != "" || c.AppSecret != "" || c.ConsumerKey != "" { configuredAuthMethods = append(configuredAuthMethods, "application_key/application_secret") } if c.ClientID != "" || c.ClientSecret != "" { configuredAuthMethods = append(configuredAuthMethods, "client_id/client_secret") } if c.AccessToken != "" { configuredAuthMethods = append(configuredAuthMethods, "access_token") } if len(configuredAuthMethods) > 1 { return fmt.Errorf("can't use multiple authentication methods: %s", strings.Join(configuredAuthMethods, ", ")) } if len(configuredAuthMethods) == 0 { return errors.New( "missing authentication information, you need to provide one of the following: application_key/application_secret, client_id/client_secret, or access_token", ) } if (c.ClientID != "") != (c.ClientSecret != "") { return errors.New("invalid oauth2 config, both client_id and client_secret must be given") } if (c.AppKey != "") != (c.AppSecret != "") { return errors.New("invalid authentication config, both application_key and application_secret must be given") } // Load real endpoint URL by name. If endpoint contains a '/', consider it as a URL if strings.Contains(endpointName, "/") { c.endpoint = endpointName } else { c.endpoint = Endpoints[endpointName] } // If we still have no valid endpoint, AppKey or AppSecret, return an error if c.endpoint == "" { return fmt.Errorf("unknown endpoint '%s', consider checking 'Endpoints' list or using an URL", endpointName) } if c.ClientID != "" { if _, ok := tokensURLs[c.endpoint]; !ok { return fmt.Errorf("oauth2 authentication is not compatible with endpoint %q", c.endpoint) } conf := &clientcredentials.Config{ ClientID: c.ClientID, ClientSecret: c.ClientSecret, TokenURL: tokensURLs[c.endpoint], Scopes: []string{"all"}, } c.oauth2TokenSource = conf.TokenSource(context.Background()) } return nil } // getConfigValue returns the value of OVH_ or "name" value from "section". If // the value could not be read from either env or any configuration files, return 'def' func getConfigValue(cfg *ini.File, section, name, def string) string { // Attempt to load from environment fromEnv := os.Getenv("OVH_" + strings.ToUpper(name)) if len(fromEnv) > 0 { return fromEnv } if !cfg.HasSection(section) { return def } // Attempt to load from configuration fromSection := cfg.Section(section) if fromSection == nil { return def } if !fromSection.HasKey(name) { return def } fromSectionKey := fromSection.Key(name) if fromSectionKey == nil { return def } return fromSectionKey.String() } go-ovh-1.9.0/ovh/configuration_test.go000066400000000000000000000130771502677366500200020ustar00rootroot00000000000000package ovh import ( "testing" "github.com/maxatome/go-testdeep/td" ) const ( systemConf = "testdata/system.ini" userPartialConf = "testdata/userPartial.ini" userConf = "testdata/user.ini" userOAuth2Conf = "testdata/user_oauth2.ini" userOAuth2InvalidConf = "testdata/user_oauth2_invalid.ini" userOAuth2IncompatibleConfig = "testdata/user_oauth2_incompatible.ini" userBothConf = "testdata/user_both.ini" localPartialConf = "testdata/localPartial.ini" localWithURLConf = "testdata/localWithURL.ini" doesNotExistConf = "testdata/doesNotExist.ini" invalidINIConf = "testdata/invalid.ini" errorConf = "testdata" ) func setConfigPaths(t testing.TB, paths ...string) { old := configPaths configPaths = paths t.Cleanup(func() { configPaths = old }) } func TestConfigForbidsTrailingSlash(t *testing.T) { client := Client{} err := client.loadConfig("https://example.org/") td.Require(t).String(err, "endpoint name cannot have a trailing slash") } func TestConfigFromFiles(t *testing.T) { setConfigPaths(t, systemConf, userPartialConf, localPartialConf) client := Client{} err := client.loadConfig("ovh-eu") td.Require(t).CmpNoError(err) td.Cmp(t, client, td.Struct(Client{ AppKey: "system", AppSecret: "user", ConsumerKey: "local", })) } func TestConfigFromOnlyOneFile(t *testing.T) { setConfigPaths(t, userConf) client := Client{} err := client.loadConfig("ovh-eu") td.Require(t).CmpNoError(err) td.Cmp(t, client, td.Struct(Client{ AppKey: "user", AppSecret: "user", ConsumerKey: "user", })) } func TestConfigFromNonExistingFile(t *testing.T) { setConfigPaths(t, doesNotExistConf) client := Client{} err := client.loadConfig("ovh-eu") td.CmpString(t, err, `missing authentication information, you need to provide one of the following: application_key/application_secret, client_id/client_secret, or access_token`) } func TestConfigFromInvalidINIFile(t *testing.T) { setConfigPaths(t, invalidINIConf) client := Client{} err := client.loadConfig("ovh-eu") td.CmpString(t, err, "cannot load configuration: unclosed section: [ovh\n") } func TestConfigFromInvalidFile(t *testing.T) { setConfigPaths(t, errorConf) client := Client{} err := client.loadConfig("ovh-eu") td.CmpString(t, err, "cannot load configuration: BOM: read testdata: is a directory") } func TestConfigFromEnv(t *testing.T) { setConfigPaths(t, userConf) t.Setenv("OVH_ENDPOINT", "ovh-eu") t.Setenv("OVH_APPLICATION_KEY", "env") t.Setenv("OVH_APPLICATION_SECRET", "env") t.Setenv("OVH_CONSUMER_KEY", "env") client := Client{} err := client.loadConfig("") td.Require(t).CmpNoError(err) td.Cmp(t, client, td.Struct(Client{ AppKey: "env", AppSecret: "env", ConsumerKey: "env", endpoint: OvhEU, })) } func TestConfigFromArgs(t *testing.T) { setConfigPaths(t, userConf) client := Client{AppKey: "param", AppSecret: "param", ConsumerKey: "param"} err := client.loadConfig("ovh-eu") td.Require(t).CmpNoError(err) td.Cmp(t, client, td.Struct(Client{ AppKey: "param", AppSecret: "param", ConsumerKey: "param", endpoint: OvhEU, })) } func TestEndpoint(t *testing.T) { assert, require := td.AssertRequire(t) setConfigPaths(t, localWithURLConf) // Test: by name client := Client{} err := client.loadConfig("ovh-eu") require.CmpNoError(err) assert.Cmp(client, td.Struct(Client{ AppKey: "ovh", })) // Test: by URL client = Client{} err = client.loadConfig("https://api.example.com:4242") require.CmpNoError(err) assert.Cmp(client, td.Struct(Client{ AppKey: "example.com", })) } func TestMissingParam(t *testing.T) { client := Client{AppKey: "param", AppSecret: "param", ConsumerKey: "param"} client.endpoint = "" err := client.loadConfig("") td.CmpNoError(t, err) client.AppKey = "" err = client.loadConfig("ovh-eu") td.CmpString(t, err, `invalid authentication config, both application_key and application_secret must be given`) client.AppKey = "param" client.AppSecret = "" err = client.loadConfig("ovh-eu") td.CmpString(t, err, `invalid authentication config, both application_key and application_secret must be given`) } func TestConfigPaths(t *testing.T) { home, err := currentUserHome() td.Require(t).CmpNoError(err) setConfigPaths(t, "", "file", "file.ini", "dir/file.ini", "~/file.ini", "~typo.ini") td.Cmp(t, home, td.Not(td.HasSuffix("/"))) td.Cmp(t, expandConfigPaths(), []interface{}{"", "file", "file.ini", "dir/file.ini", home + "/file.ini", "~typo.ini"}, ) } func TestConfigOAuth2(t *testing.T) { setConfigPaths(t, userOAuth2Conf) client := Client{} err := client.loadConfig("ovh-eu") td.Require(t).CmpNoError(err) td.Cmp(t, client, td.Struct(Client{ ClientID: "foo", ClientSecret: "bar", })) } func TestConfigInvalidBoth(t *testing.T) { setConfigPaths(t, userBothConf) client := Client{} err := client.loadConfig("ovh-eu") td.CmpString(t, err, "can't use multiple authentication methods: application_key/application_secret, client_id/client_secret") } func TestConfigOAuth2Invalid(t *testing.T) { setConfigPaths(t, userOAuth2InvalidConf) client := Client{} err := client.loadConfig("ovh-eu") td.CmpString(t, err, "invalid oauth2 config, both client_id and client_secret must be given") } func TestConfigOAuth2Incompatible(t *testing.T) { setConfigPaths(t, userOAuth2IncompatibleConfig) client := Client{} err := client.loadConfig("kimsufi-eu") td.CmpString(t, err, `oauth2 authentication is not compatible with endpoint "https://eu.api.kimsufi.com/1.0"`) } go-ovh-1.9.0/ovh/consumer_key.go000066400000000000000000000061651502677366500165770ustar00rootroot00000000000000package ovh import ( "fmt" "strings" ) // Map user friendly access level names to corresponding HTTP verbs var ( ReadOnly = []string{"GET"} ReadWrite = []string{"GET", "POST", "PUT", "DELETE"} ReadWriteSafe = []string{"GET", "POST", "PUT"} ) // AccessRule represents a method allowed for a path type AccessRule struct { // Allowed HTTP Method for the requested AccessRule. // Can be set to GET/POST/PUT/DELETE. Method string `json:"method"` // Allowed path. // Can be an exact string or a string with '*' char. // Example : // /me : only /me is authorized // /* : all calls are authorized Path string `json:"path"` } // CkValidationState represents the response when asking a new consumerKey. type CkValidationState struct { // Consumer key, which need to be validated by customer. ConsumerKey string `json:"consumerKey"` // Current status, should be always "pendingValidation". State string `json:"state"` // URL to redirect user in order to log in. ValidationURL string `json:"validationUrl"` } // CkRequest represents the parameters to fill in order to ask a new // consumerKey. type CkRequest struct { client *Client AccessRules []AccessRule `json:"accessRules"` Redirection string `json:"redirection,omitempty"` } func (ck *CkValidationState) String() string { return fmt.Sprintf("CK: %q\nStatus: %q\nValidation URL: %q\n", ck.ConsumerKey, ck.State, ck.ValidationURL, ) } // NewCkRequest helps create a new ck request func (c *Client) NewCkRequest() *CkRequest { return &CkRequest{ client: c, AccessRules: []AccessRule{}, } } // NewCkRequestWithRedirection helps create a new ck request with a redirect URL func (c *Client) NewCkRequestWithRedirection(redirection string) *CkRequest { return &CkRequest{ client: c, AccessRules: []AccessRule{}, Redirection: redirection, } } // AddRule adds a new rule to the ckRequest func (ck *CkRequest) AddRule(method, path string) { ck.AccessRules = append(ck.AccessRules, AccessRule{ Method: method, Path: path, }) } // AddRules adds grant requests on "path" for all methods. "ReadOnly", // "ReadWrite" and "ReadWriteSafe" should be used for "methods" unless // specific access are required. func (ck *CkRequest) AddRules(methods []string, path string) { for _, method := range methods { ck.AddRule(method, path) } } // AddRecursiveRules adds grant requests on "path" and "path/*", for all // methods "ReadOnly", "ReadWrite" and "ReadWriteSafe" should be used for // "methods" unless specific access are required. func (ck *CkRequest) AddRecursiveRules(methods []string, path string) { path = strings.TrimRight(path, "/") // Add rules. Skip base rule when requesting access to "/" if path != "" { ck.AddRules(methods, path) } ck.AddRules(methods, path+"/*") } // Do executes the request. On success, set the consumer key in the client // and return the URL the user needs to visit to validate the key func (ck *CkRequest) Do() (*CkValidationState, error) { state := CkValidationState{} err := ck.client.PostUnAuth("/auth/credential", ck, &state) if err == nil { ck.client.ConsumerKey = state.ConsumerKey } return &state, err } go-ovh-1.9.0/ovh/consumer_key_test.go000066400000000000000000000061471502677366500176360ustar00rootroot00000000000000package ovh import ( "encoding/json" "net/http" "testing" "github.com/jarcoal/httpmock" "github.com/maxatome/go-testdeep/td" ) func (ms *MockSuite) TestNewCkRequest(assert, require *td.T) { httpmock.RegisterResponder("POST", "https://eu.api.ovh.com/1.0/auth/credential", func(req *http.Request) (*http.Response, error) { assert.Cmp(req.Header["Accept"], []string{"application/json"}) assert.Cmp(req.Header["X-Ovh-Application"], []string{MockApplicationKey}) assert.Cmp(req.Body, td.Smuggle(json.RawMessage{}, td.JSON(`{"accessRules":[{"method":"GET","path":"/me"},{"method":"GET","path":"/xdsl/*"}]}`))) return httpmock.NewStringResponse(200, `{ "validationUrl":"https://validation.url", "ConsumerKey":"`+MockConsumerKey+`", "state":"pendingValidation" }`), nil }) ms.client.ConsumerKey = "" ckRequest := ms.client.NewCkRequest() ckRequest.AddRule("GET", "/me") ckRequest.AddRule("GET", "/xdsl/*") got, err := ckRequest.Do() require.CmpNoError(err) assert.Cmp(got, &CkValidationState{ ConsumerKey: MockConsumerKey, ValidationURL: "https://validation.url", State: "pendingValidation", }) assert.Cmp(ms.client.ConsumerKey, MockConsumerKey, "CkRequest.Do() sets client.ConsumerKey") } func (ms *MockSuite) TestInvalidCkRequest(assert, require *td.T) { httpmock.RegisterResponder("POST", "https://eu.api.ovh.com/1.0/auth/credential", httpmock.NewStringResponder(http.StatusForbidden, `{"message":"Invalid application key"}`)) ckRequest := ms.client.NewCkRequest() ckRequest.AddRule("GET", "/me") ckRequest.AddRule("GET", "/xdsl/*") _, err := ckRequest.Do() // Returns 0 value, not nil assert.Cmp(err, &APIError{ Code: http.StatusForbidden, Message: "Invalid application key", }) } func TestAddRules(t *testing.T) { client := Client{} // Test: allow all ckRequest := client.NewCkRequest() ckRequest.AddRecursiveRules(ReadWrite, "/") td.Cmp(t, ckRequest.AccessRules, []AccessRule{ {Method: "GET", Path: "/*"}, {Method: "POST", Path: "/*"}, {Method: "PUT", Path: "/*"}, {Method: "DELETE", Path: "/*"}, }) // Test: allow exactly /sms, RO ckRequest = client.NewCkRequest() ckRequest.AddRules(ReadOnly, "/sms") td.Cmp(t, ckRequest.AccessRules, []AccessRule{ {Method: "GET", Path: "/sms"}, }) // Test: allow /sms/*, RW, no delete ckRequest = client.NewCkRequest() ckRequest.AddRecursiveRules(ReadWriteSafe, "/sms") td.Cmp(t, ckRequest.AccessRules, []AccessRule{ {Method: "GET", Path: "/sms"}, {Method: "POST", Path: "/sms"}, {Method: "PUT", Path: "/sms"}, {Method: "GET", Path: "/sms/*"}, {Method: "POST", Path: "/sms/*"}, {Method: "PUT", Path: "/sms/*"}, }) } func TestCkRequestString(t *testing.T) { td.CmpString(t, &CkValidationState{ ConsumerKey: "ck", State: "pending", ValidationURL: "fakeURL", }, "CK: \"ck\"\nStatus: \"pending\"\nValidation URL: \"fakeURL\"\n") } func TestCkRequestRedirection(t *testing.T) { client := Client{} redirection := "http://localhost/api/auth/callback?token=123456" ckRequest := client.NewCkRequestWithRedirection(redirection) td.Cmp(t, ckRequest.Redirection, redirection) } go-ovh-1.9.0/ovh/error.go000066400000000000000000000032471502677366500152230ustar00rootroot00000000000000package ovh import ( "fmt" "strings" ) // APIError represents an error that can occurred while calling the API. type APIError struct { // Error class Class string `json:"class,omitempty"` // Error message. Message string `json:"message"` // Error details Details map[string]string `json:"details,omitempty"` // HTTP code. Code int // ID of the request QueryID string } // Let's make sure that APIError always satisfies the fmt.Stringer and error interfaces var _ fmt.Stringer = APIError{} var _ error = APIError{} func (err APIError) Error() string { var sb strings.Builder sb.Grow(128) // Base message fmt.Fprint(&sb, "OVHcloud API error (status code ", err.Code, "): ") // Append class if any if err.Class != "" { fmt.Fprint(&sb, err.Class, ": ") } // Catch missing IAM permissions, if any var missingIAMActionsDetails []string if missingAuthenticationActions, ok := err.Details["unauthorizedActionsByAuthentication"]; ok && missingAuthenticationActions != "" { missingIAMActionsDetails = append(missingIAMActionsDetails, missingAuthenticationActions) } if missingIAMActions, ok := err.Details["unauthorizedActionsByIAM"]; ok && missingIAMActions != "" { missingIAMActionsDetails = append(missingIAMActionsDetails, missingIAMActions) } message := err.Message if len(missingIAMActionsDetails) > 0 { message += fmt.Sprintf(" (missing IAM permissions: %s)", strings.Join(missingIAMActionsDetails, ", ")) } // Real error message, quoted fmt.Fprintf(&sb, "%q", message) // QueryID if any if err.QueryID != "" { fmt.Fprint(&sb, " (X-OVH-Query-Id: ", err.QueryID, ")") } return sb.String() } func (err APIError) String() string { return err.Error() } go-ovh-1.9.0/ovh/error_test.go000066400000000000000000000043471502677366500162640ustar00rootroot00000000000000package ovh import ( "net/http" "testing" "github.com/maxatome/go-testdeep/td" ) func TestErrorString(t *testing.T) { err := APIError{ Code: http.StatusBadRequest, Message: "An input error occurred", } expected := `OVHcloud API error (status code 400): "An input error occurred"` td.Cmp(t, err.Error(), expected) td.Cmp(t, err.String(), expected) err = APIError{ Code: http.StatusConflict, Message: `the cart id "foobar" already exists`, Class: "CartAlreadyExists", QueryID: "EU.ext-99.foobar", } expected = `OVHcloud API error (status code 409): CartAlreadyExists: "the cart id \"foobar\" already exists" (X-OVH-Query-Id: EU.ext-99.foobar)` td.Cmp(t, err.Error(), expected) td.Cmp(t, err.String(), expected) err.Class = "" expected = `OVHcloud API error (status code 409): "the cart id \"foobar\" already exists" (X-OVH-Query-Id: EU.ext-99.foobar)` td.Cmp(t, err.Error(), expected) td.Cmp(t, err.String(), expected) err = APIError{ Code: http.StatusForbidden, Message: `User not granted for this request`, Class: "Client::Forbidden", QueryID: "EU.ext-99.foobar", Details: map[string]string{ "unauthorizedActionsByAuthentication": "", "unauthorizedActionsByIAM": "account:apiovh:me/installationTemplate/get", }, } expected = `OVHcloud API error (status code 403): Client::Forbidden: "User not granted for this request (missing IAM permissions: account:apiovh:me/installationTemplate/get)" (X-OVH-Query-Id: EU.ext-99.foobar)` td.Cmp(t, err.Error(), expected) td.Cmp(t, err.String(), expected) err = APIError{ Code: http.StatusForbidden, Message: `User not granted for this request`, Class: "Client::Forbidden", QueryID: "EU.ext-99.foobar", Details: map[string]string{ "unauthorizedActionsByAuthentication": "account:apiovh:me/accessRestriction/ip/get", "unauthorizedActionsByIAM": "account:apiovh:me/installationTemplate/get", }, } expected = `OVHcloud API error (status code 403): Client::Forbidden: "User not granted for this request (missing IAM permissions: account:apiovh:me/accessRestriction/ip/get, account:apiovh:me/installationTemplate/get)" (X-OVH-Query-Id: EU.ext-99.foobar)` td.Cmp(t, err.Error(), expected) td.Cmp(t, err.String(), expected) } go-ovh-1.9.0/ovh/logger.go000066400000000000000000000004741502677366500153500ustar00rootroot00000000000000package ovh import ( "net/http" ) // Logger is the interface that should be implemented for loggers that wish to // log HTTP requests and HTTP responses. type Logger interface { // LogRequest logs an HTTP request. LogRequest(*http.Request) // LogResponse logs an HTTP response. LogResponse(*http.Response) } go-ovh-1.9.0/ovh/ovh.go000066400000000000000000000356041502677366500146700ustar00rootroot00000000000000// Package ovh provides a HTTP wrapper for the OVH API. package ovh import ( "bytes" "context" "crypto/sha1" "encoding/json" "errors" "fmt" "io/ioutil" "net/http" "runtime" "strconv" "strings" "sync/atomic" "time" "golang.org/x/oauth2" ) // getLocalTime is a function to be overwritten during the tests, it returns the time // on the the local machine var getLocalTime = time.Now // DefaultTimeout api requests after 180s const DefaultTimeout = 180 * time.Second // Endpoints const ( OvhEU = "https://eu.api.ovh.com/1.0" OvhCA = "https://ca.api.ovh.com/1.0" OvhUS = "https://api.us.ovhcloud.com/1.0" KimsufiEU = "https://eu.api.kimsufi.com/1.0" KimsufiCA = "https://ca.api.kimsufi.com/1.0" SoyoustartEU = "https://eu.api.soyoustart.com/1.0" SoyoustartCA = "https://ca.api.soyoustart.com/1.0" ) // Endpoints conveniently maps endpoints names to their URI for external configuration var Endpoints = map[string]string{ "ovh-eu": OvhEU, "ovh-ca": OvhCA, "ovh-us": OvhUS, "kimsufi-eu": KimsufiEU, "kimsufi-ca": KimsufiCA, "soyoustart-eu": SoyoustartEU, "soyoustart-ca": SoyoustartCA, } // Errors var ( ErrAPIDown = errors.New("go-ovh: the OVH API is not reachable: failed to get /auth/time response") tokensURLs = map[string]string{ OvhEU: "https://www.ovh.com/auth/oauth2/token", OvhCA: "https://ca.ovh.com/auth/oauth2/token", OvhUS: "https://us.ovhcloud.com/auth/oauth2/token", } ) // Client represents a client to call the OVH API type Client struct { // AccessToken is a short-lived access token that we got from auth/oauth2/token endpoint. AccessToken string // Self generated tokens. Create one by visiting // https://eu.api.ovh.com/createApp/ // AppKey holds the Application key AppKey string // AppSecret holds the Application secret key AppSecret string // ConsumerKey holds the user/app specific token. It must have been validated before use. ConsumerKey string ClientID string ClientSecret string // API endpoint endpoint string oauth2TokenSource oauth2.TokenSource // Client is the underlying HTTP client used to run the requests. It may be overloaded but a default one is instanciated in ``NewClient`` by default. Client *http.Client // Logger is used to log HTTP requests and responses. Logger Logger // Ensures that the timeDelta function is only ran once // sync.Once would consider init done, even in case of error // hence a good old flag timeDelta atomic.Value // Timeout configures the maximum duration to wait for an API requests to complete Timeout time.Duration // UserAgent configures the user-agent indication that will be sent in the requests to OVHcloud API UserAgent string } // NewClient represents a new client to call the API func NewClient(endpoint, appKey, appSecret, consumerKey string) (*Client, error) { client := Client{ AppKey: appKey, AppSecret: appSecret, ConsumerKey: consumerKey, Client: &http.Client{}, Timeout: DefaultTimeout, } // Get and check the configuration if err := client.loadConfig(endpoint); err != nil { return nil, err } return &client, nil } // NewEndpointClient will create an API client for specified // endpoint and load all credentials from environment or // configuration files func NewEndpointClient(endpoint string) (*Client, error) { return NewClient(endpoint, "", "", "") } // NewDefaultClient will load all it's parameter from environment // or configuration files func NewDefaultClient() (*Client, error) { return NewClient("", "", "", "") } func NewOAuth2Client(endpoint, clientID, clientSecret string) (*Client, error) { client := Client{ ClientID: clientID, ClientSecret: clientSecret, Client: &http.Client{}, Timeout: DefaultTimeout, } // Get and check the configuration if err := client.loadConfig(endpoint); err != nil { return nil, err } return &client, nil } func NewAccessTokenClient(endpoint, accessToken string) (*Client, error) { client := Client{ AccessToken: accessToken, Client: &http.Client{}, Timeout: DefaultTimeout, } // Get and check the configuration if err := client.loadConfig(endpoint); err != nil { return nil, err } return &client, nil } func (c *Client) Endpoint() string { return c.endpoint } func (c *Client) SetEndpoint(endpoint string) error { if strings.HasSuffix(endpoint, "/") { return errors.New("endpoint name cannot have a trailing slash") } c.endpoint = endpoint return nil } // // High level helpers // // Ping performs a ping to OVH API. // In fact, ping is just a /auth/time call, in order to check if API is up. func (c *Client) Ping() error { _, err := c.getTime() return err } // TimeDelta represents the delay between the machine that runs the code and the // OVH API. The delay shouldn't change, let's do it only once. func (c *Client) TimeDelta() (time.Duration, error) { return c.getTimeDelta() } // Time returns time from the OVH API, by asking GET /auth/time. func (c *Client) Time() (*time.Time, error) { return c.getTime() } // // Common request wrappers // // Get is a wrapper for the GET method func (c *Client) Get(url string, resType interface{}) error { return c.CallAPI("GET", url, nil, resType, true) } // GetUnAuth is a wrapper for the unauthenticated GET method func (c *Client) GetUnAuth(url string, resType interface{}) error { return c.CallAPI("GET", url, nil, resType, false) } // Post is a wrapper for the POST method func (c *Client) Post(url string, reqBody, resType interface{}) error { return c.CallAPI("POST", url, reqBody, resType, true) } // PostUnAuth is a wrapper for the unauthenticated POST method func (c *Client) PostUnAuth(url string, reqBody, resType interface{}) error { return c.CallAPI("POST", url, reqBody, resType, false) } // Put is a wrapper for the PUT method func (c *Client) Put(url string, reqBody, resType interface{}) error { return c.CallAPI("PUT", url, reqBody, resType, true) } // PutUnAuth is a wrapper for the unauthenticated PUT method func (c *Client) PutUnAuth(url string, reqBody, resType interface{}) error { return c.CallAPI("PUT", url, reqBody, resType, false) } // Delete is a wrapper for the DELETE method func (c *Client) Delete(url string, resType interface{}) error { return c.CallAPI("DELETE", url, nil, resType, true) } // DeleteUnAuth is a wrapper for the unauthenticated DELETE method func (c *Client) DeleteUnAuth(url string, resType interface{}) error { return c.CallAPI("DELETE", url, nil, resType, false) } // GetWithContext is a wrapper for the GET method func (c *Client) GetWithContext(ctx context.Context, url string, resType interface{}) error { return c.CallAPIWithContext(ctx, "GET", url, nil, resType, true) } // GetUnAuthWithContext is a wrapper for the unauthenticated GET method func (c *Client) GetUnAuthWithContext(ctx context.Context, url string, resType interface{}) error { return c.CallAPIWithContext(ctx, "GET", url, nil, resType, false) } // PostWithContext is a wrapper for the POST method func (c *Client) PostWithContext(ctx context.Context, url string, reqBody, resType interface{}) error { return c.CallAPIWithContext(ctx, "POST", url, reqBody, resType, true) } // PostUnAuthWithContext is a wrapper for the unauthenticated POST method func (c *Client) PostUnAuthWithContext(ctx context.Context, url string, reqBody, resType interface{}) error { return c.CallAPIWithContext(ctx, "POST", url, reqBody, resType, false) } // PutWithContext is a wrapper for the PUT method func (c *Client) PutWithContext(ctx context.Context, url string, reqBody, resType interface{}) error { return c.CallAPIWithContext(ctx, "PUT", url, reqBody, resType, true) } // PutUnAuthWithContext is a wrapper for the unauthenticated PUT method func (c *Client) PutUnAuthWithContext(ctx context.Context, url string, reqBody, resType interface{}) error { return c.CallAPIWithContext(ctx, "PUT", url, reqBody, resType, false) } // DeleteWithContext is a wrapper for the DELETE method func (c *Client) DeleteWithContext(ctx context.Context, url string, resType interface{}) error { return c.CallAPIWithContext(ctx, "DELETE", url, nil, resType, true) } // DeleteUnAuthWithContext is a wrapper for the unauthenticated DELETE method func (c *Client) DeleteUnAuthWithContext(ctx context.Context, url string, resType interface{}) error { return c.CallAPIWithContext(ctx, "DELETE", url, nil, resType, false) } // timeDelta returns the time delta between the host and the remote API func (c *Client) getTimeDelta() (time.Duration, error) { d, ok := c.timeDelta.Load().(time.Duration) if ok { return d, nil } ovhTime, err := c.getTime() if err != nil { return 0, err } d = getLocalTime().Sub(*ovhTime) c.timeDelta.Store(d) return d, nil } // getTime t returns time from for a given api client endpoint func (c *Client) getTime() (*time.Time, error) { var timestamp int64 err := c.GetUnAuth("/auth/time", ×tamp) if err != nil { return nil, err } serverTime := time.Unix(timestamp, 0) return &serverTime, nil } // getTarget returns the URL to target given and endpoint and a path. // If the path starts with `/v1` or `/v2`, then remove the trailing `/1.0` from the endpoint. func getTarget(endpoint, path string) string { // /1.0 + /v1/ or /1.0 + /v2/ if strings.HasSuffix(endpoint, "/1.0") && (strings.HasPrefix(path, "/v1/") || strings.HasPrefix(path, "/v2/")) { return endpoint[:len(endpoint)-4] + path } return endpoint + path } // NewRequest returns a new HTTP request func (c *Client) NewRequest(method, path string, reqBody interface{}, needAuth bool) (*http.Request, error) { var body []byte var err error if reqBody != nil { body, err = json.Marshal(reqBody) if err != nil { return nil, err } } target := getTarget(c.endpoint, path) req, err := http.NewRequest(method, target, bytes.NewReader(body)) if err != nil { return nil, err } // Inject headers if body != nil { req.Header.Add("Content-Type", "application/json;charset=utf-8") } if c.AppKey != "" { req.Header.Add("X-Ovh-Application", c.AppKey) } req.Header.Add("Accept", "application/json") // Inject signature. Some methods do not need authentication, especially /time, // /auth and some /order methods are actually broken if authenticated. if needAuth { if c.AppKey != "" { timeDelta, err := c.TimeDelta() if err != nil { return nil, err } timestamp := getLocalTime().Add(-timeDelta).Unix() req.Header.Add("X-Ovh-Timestamp", strconv.FormatInt(timestamp, 10)) req.Header.Add("X-Ovh-Consumer", c.ConsumerKey) h := sha1.New() h.Write([]byte(fmt.Sprintf("%s+%s+%s+%s+%s+%d", c.AppSecret, c.ConsumerKey, method, target, body, timestamp, ))) req.Header.Add("X-Ovh-Signature", fmt.Sprintf("$1$%x", h.Sum(nil))) } else if c.ClientID != "" { token, err := c.oauth2TokenSource.Token() if err != nil { return nil, fmt.Errorf("failed to retrieve OAuth2 Access Token: %w", err) } req.Header.Set("Authorization", "Bearer "+token.AccessToken) } else if c.AccessToken != "" { req.Header.Set("Authorization", "Bearer "+c.AccessToken) } } // Send the request with requested timeout c.Client.Timeout = c.Timeout if c.UserAgent != "" { // When running in a WebAssembly binary, let the caller set // the user-agent freely to be able to use the browser's one. if runtime.GOARCH == "wasm" && runtime.GOOS == "js" { req.Header.Set("User-Agent", c.UserAgent) } else { req.Header.Set("User-Agent", "github.com/ovh/go-ovh ("+c.UserAgent+")") } } else { req.Header.Set("User-Agent", "github.com/ovh/go-ovh") } return req, nil } // Do sends an HTTP request and returns an HTTP response func (c *Client) Do(req *http.Request) (*http.Response, error) { if c.Logger != nil { c.Logger.LogRequest(req) } resp, err := c.Client.Do(req) if err != nil { return nil, err } if c.Logger != nil { c.Logger.LogResponse(resp) } return resp, nil } // CallAPI is the lowest level call helper. If needAuth is true, // inject authentication headers and sign the request. // // Request signature is a sha1 hash on following fields, joined by '+': // - applicationSecret (from Client instance) // - consumerKey (from Client instance) // - capitalized method (from arguments) // - full request url, including any query string argument // - full serialized request body // - server current time (takes time delta into account) // // Call will automatically assemble the target url from the endpoint // configured in the client instance and the path argument. If the reqBody // argument is not nil, it will also serialize it as json and inject // the required Content-Type header. // // If everything went fine, unmarshall response into resType and return nil // otherwise, return the error func (c *Client) CallAPI(method, path string, reqBody, resType interface{}, needAuth bool) error { return c.CallAPIWithContext(context.Background(), method, path, reqBody, resType, needAuth) } // CallAPIWithContext is the lowest level call helper. If needAuth is true, // inject authentication headers and sign the request. // // Request signature is a sha1 hash on following fields, joined by '+': // - applicationSecret (from Client instance) // - consumerKey (from Client instance) // - capitalized method (from arguments) // - full request url, including any query string argument // - full serialized request body // - server current time (takes time delta into account) // // Context is used by http.Client to handle context cancelation. // // Call will automatically assemble the target url from the endpoint // configured in the client instance and the path argument. If the reqBody // argument is not nil, it will also serialize it as json and inject // the required Content-Type header. // // If everything went fine, unmarshall response into resType and return nil // otherwise, return the error func (c *Client) CallAPIWithContext(ctx context.Context, method, path string, reqBody, resType interface{}, needAuth bool) error { req, err := c.NewRequest(method, path, reqBody, needAuth) if err != nil { return err } req = req.WithContext(ctx) response, err := c.Do(req) if err != nil { return err } return c.UnmarshalResponse(response, resType) } // UnmarshalResponse checks the response and unmarshals it into the response // type if needed Helper function, called from CallAPI func (c *Client) UnmarshalResponse(response *http.Response, resType interface{}) error { // Read all the response body defer response.Body.Close() body, err := ioutil.ReadAll(response.Body) if err != nil { return err } // < 200 && >= 300 : API error if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices { apiError := &APIError{Code: response.StatusCode} if err = json.Unmarshal(body, apiError); err != nil { apiError.Message = string(body) } apiError.QueryID = response.Header.Get("X-Ovh-QueryID") return apiError } // Nothing to unmarshal if len(body) == 0 || resType == nil { return nil } d := json.NewDecoder(bytes.NewReader(body)) d.UseNumber() return d.Decode(&resType) } go-ovh-1.9.0/ovh/ovh_test.go000066400000000000000000000617211502677366500157260ustar00rootroot00000000000000package ovh import ( "context" "encoding/json" "fmt" "io" "net/http" "os" "strconv" "strings" "testing" "time" "github.com/jarcoal/httpmock" "github.com/maxatome/go-testdeep/helpers/tdsuite" "github.com/maxatome/go-testdeep/td" ) const ( // In case you wonder, these are real *revoked* credentials MockApplicationKey = "TDPKJdwZwAQPwKX2" MockApplicationSecret = "9ufkBmLaTQ9nz5yMUlg79taH0GNnzDjk" MockConsumerKey = "5mBuy6SUQcRw2ZUxg0cG68BoDKpED4KY" MockTime = 1457018875 ) // // Utils // func sbody(s string) io.ReadCloser { return io.NopCloser(strings.NewReader(s)) } // // Tests // func TestClientEndpoint(t *testing.T) { require := td.Require(t) client, err := NewClient("ovh-eu", MockApplicationKey, MockApplicationSecret, MockConsumerKey) require.CmpNoError(err) td.Cmp(t, client.Endpoint(), OvhEU) client, err = NewClient("ovh-ca", MockApplicationKey, MockApplicationSecret, MockConsumerKey) require.CmpNoError(err) td.Cmp(t, client.Endpoint(), OvhCA) client, err = NewClient("https://example.org", MockApplicationKey, MockApplicationSecret, MockConsumerKey) require.CmpNoError(err) td.Cmp(t, client.Endpoint(), "https://example.org") } type MockSuite struct { client *Client } func (ms *MockSuite) Setup(t *td.T) error { httpmock.Activate() return nil } func (ms *MockSuite) PreTest(t *td.T, testName string) error { client, err := NewClient("ovh-eu", MockApplicationKey, MockApplicationSecret, MockConsumerKey) if err != nil { return err } ms.client = client return nil } func (ms *MockSuite) PostTest(t *td.T, testName string) error { httpmock.Reset() return nil } func (ms *MockSuite) Destroy(t *td.T) error { httpmock.DeactivateAndReset() return nil } func TestMockSuite(t *testing.T) { tdsuite.Run(t, &MockSuite{}) } func (ms *MockSuite) TestPing(assert *td.T) { httpmock.RegisterResponder("GET", "https://eu.api.ovh.com/1.0/auth/time", httpmock.NewStringResponder(200, "0")) assert.CmpNoError(ms.client.Ping()) assert.Cmp(httpmock.GetCallCountInfo()["GET https://eu.api.ovh.com/1.0/auth/time"], 1) } func (ms *MockSuite) TestTime(assert, require *td.T) { httpmock.RegisterResponder("GET", "https://eu.api.ovh.com/1.0/auth/time", httpmock.NewStringResponder(200, strconv.Itoa(MockTime))) serverTime, err := ms.client.Time() require.CmpNoError(err) assert.CmpLax(serverTime.Unix(), MockTime) assert.Cmp(httpmock.GetCallCountInfo()["GET https://eu.api.ovh.com/1.0/auth/time"], 1) } func (ms *MockSuite) TestGetTimeDelta(assert, require *td.T) { httpmock.RegisterResponder("GET", "https://eu.api.ovh.com/1.0/auth/time", httpmock.NewStringResponder(200, strconv.FormatInt(time.Now().Unix()-10, 10))) delta, err := ms.client.TimeDelta() require.CmpNoError(err) assert.Between(delta.Seconds(), 9.0, 11.0, td.BoundsInIn) assert.Cmp(httpmock.GetCallCountInfo()["GET https://eu.api.ovh.com/1.0/auth/time"], 1) } func (ms *MockSuite) TestError500HTML(assert, require *td.T) { errHTML := `

test

` httpmock.RegisterResponder("GET", "https://eu.api.ovh.com/1.0/test", httpmock.NewStringResponder(http.StatusServiceUnavailable, errHTML)) err := ms.client.CallAPI("GET", "/test", nil, nil, false) assert.Cmp(err, &APIError{ Code: http.StatusServiceUnavailable, Message: errHTML, }) } func (ms *MockSuite) TestAllAPIMethods(assert, require *td.T) { const payloadAuth = `{"call":"auth"}` const payloadUnAuth = `{"call":"unauth"}` var body = json.RawMessage(`{"a":"b","c":"d"}`) previous := getLocalTime getLocalTime = func() time.Time { return time.Unix(MockTime, 0) } assert.Cleanup(func() { getLocalTime = previous }) checkAuthHeaders := func(assert *td.T, req *http.Request, signature string) { assert.Helper() if signature == "" { assert.Cmp(req.Header, td.Not(td.ContainsKey("X-Ovh-Timestamp")), "No X-Ovh-Timestamp for %s unauth call", req.Method) assert.Cmp(req.Header, td.Not(td.ContainsKey("X-Ovh-Consumer")), "No X-Ovh-Consumer for %s unauth call", req.Method) assert.Cmp(req.Header, td.Not(td.ContainsKey("X-Ovh-Signature")), "No X-Ovh-Signature for %s unauth call", req.Method) } else { assert.Cmp(req.Header["X-Ovh-Timestamp"], []string{strconv.Itoa(MockTime)}, "Right X-Ovh-Timestamp for %s auth call", req.Method) assert.Cmp(req.Header["X-Ovh-Consumer"], []string{MockConsumerKey}, "Right X-Ovh-Consumer for %s auth call", req.Method) assert.Cmp(req.Header["X-Ovh-Signature"], []string{signature}, "Right X-Ovh-Signature for %s auth call", req.Method) } } checkBody := func(assert *td.T, req *http.Request) { assert.Helper() if req.Method != "POST" && req.Method != "PUT" { assert.Cmp(req.Body, td.Smuggle(io.ReadAll, td.Empty()), "Body is empty for %s call", req.Method) } else { assert.Cmp(req.Body, td.Smuggle(json.RawMessage{}, td.JSON(`{"a":"b","c":"d"}`)), "Right body for %s call", req.Method) } } httpmock.RegisterResponder("GET", "https://eu.api.ovh.com/1.0/auth/time", httpmock.NewStringResponder(200, strconv.Itoa(MockTime))) mockSignatures := map[string]struct{ authSig, timeoutSig string }{ "GET": {authSig: "$1$e9556054b6309771395efa467c22e627407461ad", timeoutSig: "$1$1f0958be70f095ddaba525778a9ac1dcffac89f3"}, "POST": {authSig: "$1$ec2fb5c7a81f64723c77d2e5b609ae6f58a84fc1", timeoutSig: "$1$b592effcb3bc2d37860eceb06a1b17670fbe49c6"}, "PUT": {authSig: "$1$8a75a9e7c8e7296c9dbeda6a2a735eb6bd58ec4b", timeoutSig: "$1$6b27c2a693a0eb4980217046b2fe10d74ba796f0"}, "DELETE": {authSig: "$1$a1eecd00b3b02b6cf5708b84b9ff42059a950d85", timeoutSig: "$1$bd59b15361548c388058009e00c508081e991e8b"}, } buildMock := func(assert *td.T, method string) { method = strings.ToUpper(method) httpmock.RegisterResponder(method, "https://eu.api.ovh.com/1.0/auth", func(req *http.Request) (*http.Response, error) { checkAuthHeaders(assert, req, mockSignatures[method].authSig) checkBody(assert, req) return httpmock.NewStringResponse(200, payloadAuth), nil }) httpmock.RegisterResponder(method, "https://eu.api.ovh.com/1.0/unauth", func(req *http.Request) (*http.Response, error) { checkAuthHeaders(assert, req, "") checkBody(assert, req) return httpmock.NewStringResponse(200, payloadUnAuth), nil }) httpmock.RegisterResponder(method, "https://eu.api.ovh.com/1.0/authTO", func(req *http.Request) (*http.Response, error) { checkAuthHeaders(assert, req, mockSignatures[method].timeoutSig) checkBody(assert, req) time.Sleep(200 * time.Millisecond) return httpmock.NewStringResponse(200, `{"call":"authTO"}`), nil }) httpmock.RegisterResponder(method, "https://eu.api.ovh.com/1.0/unauthTO", func(req *http.Request) (*http.Response, error) { checkAuthHeaders(assert, req, "") checkBody(assert, req) time.Sleep(200 * time.Millisecond) return httpmock.NewStringResponse(200, `{"call":"unauthTO"}`), nil }) } // Tests without body: GET and DELETE for _, test := range []struct { method string call, callUnAuth func(string, interface{}) error callWithContext, callUnAuthWithContext func(context.Context, string, interface{}) error }{ {"Get", ms.client.Get, ms.client.GetUnAuth, ms.client.GetWithContext, ms.client.GetUnAuthWithContext}, {"Delete", ms.client.Delete, ms.client.DeleteUnAuth, ms.client.DeleteWithContext, ms.client.DeleteUnAuthWithContext}, } { assert.RunAssertRequire(test.method+" method", func(assert, require *td.T) { buildMock(assert, test.method) var res json.RawMessage err := test.call("/auth", &res) require.CmpNoError(err, "No errors for method %s with auth", test.method) assert.Cmp(res, td.JSON(payloadAuth), "Got expected payload for method %s with auth", test.method) res = json.RawMessage{} err = test.callWithContext(context.Background(), "/auth", &res) require.CmpNoError(err, "No errors for method %s with auth and context", test.method) assert.Cmp(res, td.JSON(payloadAuth), "Got expected payload for method %s with auth and context", test.method) res = json.RawMessage{} err = test.callUnAuth("/unauth", &res) require.CmpNoError(err, "No errors for method %s without auth", test.method) assert.Cmp(res, td.JSON(payloadUnAuth), "Got expected payload for method %s without auth", test.method) res = json.RawMessage{} err = test.callUnAuthWithContext(context.Background(), "/unauth", &res) require.CmpNoError(err, "No errors for method %s without auth and with context", test.method) assert.Cmp(res, td.JSON(payloadUnAuth), "Got expected payload for method %s without auth and with context", test.method) res = json.RawMessage{} ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) assert.Cleanup(cancel) err = test.callWithContext(ctx, "/authTO", &res) assert.Empty(res, "Empty result after timeout for method %s with auth", test.method) assert.String(err, test.method+` "https://eu.api.ovh.com/1.0/authTO": context deadline exceeded`, "Timeout messsage for method %s with auth", test.method, ) res = json.RawMessage{} ctx, cancel = context.WithTimeout(context.Background(), 100*time.Millisecond) assert.Cleanup(cancel) err = test.callUnAuthWithContext(ctx, "/unauthTO", &res) assert.Empty(res, "Empty result after timeout for method %s without auth", test.method) assert.String(err, test.method+` "https://eu.api.ovh.com/1.0/unauthTO": context deadline exceeded`, "Timeout messsage for method %s without auth", test.method, ) }) } // Tests with body: POST and PUT for _, test := range []struct { method string call, callUnAuth func(string, interface{}, interface{}) error callWithContext, callUnAuthWithContext func(context.Context, string, interface{}, interface{}) error }{ {"Post", ms.client.Post, ms.client.PostUnAuth, ms.client.PostWithContext, ms.client.PostUnAuthWithContext}, {"Put", ms.client.Put, ms.client.PutUnAuth, ms.client.PutWithContext, ms.client.PutUnAuthWithContext}, } { assert.RunAssertRequire(test.method+" method", func(assert, require *td.T) { buildMock(assert, test.method) var res json.RawMessage err := test.call("/auth", body, &res) require.CmpNoError(err, "No errors for method %s with auth", test.method) assert.Cmp(res, td.JSON(payloadAuth), "Got expected payload for method %s with auth", test.method) res = json.RawMessage{} err = test.callWithContext(context.Background(), "/auth", body, &res) require.CmpNoError(err, "No errors for method %s with auth and context", test.method) assert.Cmp(res, td.JSON(payloadAuth), "Got expected payload for method %s with auth and context", test.method) res = json.RawMessage{} err = test.callUnAuth("/unauth", body, &res) require.CmpNoError(err, "No errors for method %s without auth", test.method) assert.Cmp(res, td.JSON(payloadUnAuth), "Got expected payload for method %s without auth", test.method) res = json.RawMessage{} err = test.callUnAuthWithContext(context.Background(), "/unauth", body, &res) require.CmpNoError(err, "No errors for method %s without auth and with context", test.method) assert.Cmp(res, td.JSON(payloadUnAuth), "Got expected payload for method %s without auth and with context", test.method) res = json.RawMessage{} ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) assert.Cleanup(cancel) err = test.callWithContext(ctx, "/authTO", body, &res) assert.String(err, test.method+` "https://eu.api.ovh.com/1.0/authTO": context deadline exceeded`, "Timeout messsage for method %s with auth", test.method, ) assert.Empty(res, "Empty result after timeout for method %s with auth", test.method) res = json.RawMessage{} ctx, cancel = context.WithTimeout(context.Background(), 100*time.Millisecond) assert.Cleanup(cancel) err = test.callUnAuthWithContext(ctx, "/unauthTO", body, &res) assert.String(err, test.method+` "https://eu.api.ovh.com/1.0/unauthTO": context deadline exceeded`, "Timeout messsage for method %s without auth", test.method, ) assert.Empty(res, "Empty result after timeout for method %s without auth", test.method) }) } } // Mock ReadCloser, always failing type ErrorReadCloser struct{} func (ErrorReadCloser) Read(p []byte) (int, error) { return 0, fmt.Errorf("ErrorReader") } func (ErrorReadCloser) Close() error { return nil } func TestGetResponse(t *testing.T) { client := Client{} // Nominal var apiInt int err := client.UnmarshalResponse(&http.Response{ StatusCode: 200, Body: sbody(`42`), }, &apiInt) td.CmpNoError(t, err, "Can Client.UnmarshalResponse with status 200 and int body") td.Cmp(t, apiInt, 42) // Nominal: empty body err = client.UnmarshalResponse(&http.Response{ StatusCode: 200, Body: sbody(``), }, nil) td.CmpNoError(t, err, "Can Client.UnmarshalResponse with status 200 and empty body") // Error apiInt = 0 err = client.UnmarshalResponse(&http.Response{ StatusCode: 400, Body: sbody(`{"code": 400, "message": "Ooops..."}`), }, &apiInt) td.Cmp(t, err, &APIError{ Code: 400, Message: "Ooops...", }, "Can parse a API error") td.Cmp(t, apiInt, 0) // Error: body read error err = client.UnmarshalResponse(&http.Response{ Body: ErrorReadCloser{}, }, nil) td.CmpString(t, err, "ErrorReader") // Error: HTTP Error + broken json err = client.UnmarshalResponse(&http.Response{ StatusCode: 400, Body: sbody(`{"code": 400, "mes`), }, nil) td.Cmp(t, err, &APIError{Code: 400, Message: `{"code": 400, "mes`}) // Error with QueryID responseHeaders := http.Header{} responseHeaders.Add("X-Ovh-QueryID", "FR.ws-8.5860f657.4632.0180") err = client.UnmarshalResponse(&http.Response{ StatusCode: 400, Body: sbody(`{"code": 400, "message": "Ooops..."}`), Header: responseHeaders, }, &apiInt) td.Cmp(t, err, &APIError{ Code: 400, Message: "Ooops...", QueryID: "FR.ws-8.5860f657.4632.0180", }) } func TestGetResponseUnmarshalNumber(t *testing.T) { assert, require := td.AssertRequire(t) client := Client{} call := func(output interface{}) { t.Helper() err := client.UnmarshalResponse(&http.Response{ StatusCode: 200, Body: sbody(`{"orderId": 1234567890}`), }, output) require.CmpNoError(err) } // with map[string]interface{} as output var output map[string]interface{} call(&output) assert.Cmp(output, map[string]interface{}{"orderId": json.Number("1234567890")}) // with map[string]int64 as output var outputInt map[string]int64 call(&outputInt) assert.Cmp(outputInt, map[string]int64{"orderId": 1234567890}) // with map[string]int64 as output var outputFloat map[string]float64 call(&outputFloat) assert.Cmp(outputFloat, map[string]float64{"orderId": 1234567890}) } func TestConstructors(t *testing.T) { assert, require := td.AssertRequire(t) // Missing Endpoint, defaulting to ovh-eu client, err := NewClient("", MockApplicationKey, MockApplicationSecret, MockConsumerKey) assert.NotNil(client) assert.CmpNoError(err) // Error: missing ApplicationKey client, err = NewClient("ovh-eu", "", MockApplicationSecret, MockConsumerKey) assert.Nil(client) assert.String(err, `invalid authentication config, both application_key and application_secret must be given`) // Error: missing ApplicationSecret client, err = NewClient("ovh-eu", MockConsumerKey, "", MockConsumerKey) assert.Nil(client) assert.String(err, `invalid authentication config, both application_key and application_secret must be given`) // Next: success cases expected := td.Struct(&Client{ AppKey: MockApplicationKey, AppSecret: MockApplicationSecret, ConsumerKey: MockConsumerKey, endpoint: "https://eu.api.ovh.com/1.0", }) // Nominal: full constructor client, err = NewClient("ovh-eu", MockApplicationKey, MockApplicationSecret, MockConsumerKey) require.CmpNoError(err) assert.Cmp(client, expected) // Nominal: Endpoint constructor t.Setenv("OVH_APPLICATION_KEY", MockApplicationKey) t.Setenv("OVH_APPLICATION_SECRET", MockApplicationSecret) t.Setenv("OVH_CONSUMER_KEY", MockConsumerKey) client, err = NewEndpointClient("ovh-eu") require.CmpNoError(err) assert.Cmp(client, expected) // Nominal: Default constructor t.Setenv("OVH_ENDPOINT", "ovh-eu") client, err = NewDefaultClient() require.CmpNoError(err) assert.Cmp(client, expected) } func TestConstructorsOAuth2(t *testing.T) { assert, require := td.AssertRequire(t) // Missing Endpoint: defaulting to ovh-eu client, err := NewOAuth2Client("", "aaaaaaaa", "bbbbbbbbbbbbbbbbbbbbbbbbbbbb") assert.NotNil(client) assert.CmpNoError(err) // Error: missing Client ID client, err = NewOAuth2Client("ovh-eu", "", "MockApplicationSecret") assert.Nil(client) assert.String(err, `invalid oauth2 config, both client_id and client_secret must be given`) // Error: missing Client Secret client, err = NewOAuth2Client("ovh-eu", "aaaaaaaaaaaaaaa", "") assert.Nil(client) assert.String(err, `invalid oauth2 config, both client_id and client_secret must be given`) // Next: success cases expected := td.Struct(&Client{ ClientID: "aaaaaaaa", ClientSecret: "bbbbbbbbbbbbbbbbbbbbbbbbbbbb", endpoint: "https://eu.api.ovh.com/1.0", }) // Nominal: full constructor client, err = NewOAuth2Client("ovh-eu", "aaaaaaaa", "bbbbbbbbbbbbbbbbbbbbbbbbbbbb") require.CmpNoError(err) assert.Cmp(client, expected) // With NewEndpointClient setConfigPaths(t, userOAuth2Conf) client, err = NewEndpointClient("ovh-eu") require.CmpNoError(err) assert.Cmp(client, td.Struct(&Client{ ClientID: "foo", ClientSecret: "bar", })) } func TestConstructorsAccessToken(t *testing.T) { assert, require := td.AssertRequire(t) // Missing Endpoint: defaulting to ovh-eu client, err := NewAccessTokenClient("", "aaaaaaaa") assert.NotNil(client) assert.CmpNoError(err) // Next: success cases expected := td.Struct(&Client{ AccessToken: "aaaaaaaa", endpoint: "https://eu.api.ovh.com/1.0", }) // Nominal: full constructor client, err = NewAccessTokenClient("ovh-eu", "aaaaaaaa") require.CmpNoError(err) assert.Cmp(client, expected) } func (ms *MockSuite) TestVersionInURL(assert, require *td.T) { // Signature checking mocks httpmock.RegisterResponder("GET", "https://eu.api.ovh.com/1.0/call", func(req *http.Request) (*http.Response, error) { assert.Cmp(req.Header["X-Ovh-Signature"], []string{"$1$7f2db49253edfc41891023fcd1a54cf61db05fbb"}, "Right X-Ovh-Signature for /1.0 auth call") return httpmock.NewStringResponse(200, "{}"), nil }) httpmock.RegisterResponder("GET", "https://eu.api.ovh.com/v1/call", func(req *http.Request) (*http.Response, error) { assert.Cmp(req.Header["X-Ovh-Signature"], []string{"$1$e6e7906d385eb28adcbfbe6b66c1528a42d741ad"}, "Right X-Ovh-Signature for /v1 auth call") return httpmock.NewStringResponse(200, "{}"), nil }) httpmock.RegisterResponder("GET", "https://eu.api.ovh.com/v2/call", func(req *http.Request) (*http.Response, error) { assert.Cmp(req.Header["X-Ovh-Signature"], []string{"$1$bb63b132a6f84ad5433d0c534d48d3f7c3804285"}, "Right X-Ovh-Signature for /v2 auth call") return httpmock.NewStringResponse(200, "{}"), nil }) // Mock local and distant time previous := getLocalTime getLocalTime = func() time.Time { return time.Unix(MockTime, 0) } assert.Cleanup(func() { getLocalTime = previous }) httpmock.RegisterResponder("GET", "https://eu.api.ovh.com/1.0/auth/time", httpmock.NewStringResponder(200, strconv.Itoa(MockTime))) assertCallCount := func(assert *td.T, ccNoVersion, ccV1, ccV2 int) { assert.Helper() assert.Cmp(httpmock.GetCallCountInfo(), map[string]int{ "GET https://eu.api.ovh.com/1.0/auth/time": 1, "GET https://eu.api.ovh.com/1.0/call": ccNoVersion, "GET https://eu.api.ovh.com/v1/call": ccV1, "GET https://eu.api.ovh.com/v2/call": ccV2, }) } require.Cmp(ms.client.endpoint, "https://eu.api.ovh.com/1.0") require.CmpNoError(ms.client.Get("/call", nil)) assertCallCount(assert, 1, 0, 0) require.CmpNoError(ms.client.Get("/v1/call", nil)) assertCallCount(assert, 1, 1, 0) require.CmpNoError(ms.client.Get("/v2/call", nil)) assertCallCount(assert, 1, 1, 1) } func TestOAuth2_503(t *testing.T) { assert, require := td.AssertRequire(t) httpmock.Activate() defer httpmock.DeactivateAndReset() errHTML := `

test

` httpmock.RegisterResponder("POST", "https://www.ovh.com/auth/oauth2/token", httpmock.NewStringResponder(http.StatusServiceUnavailable, errHTML)) // Nominal: full constructor client, err := NewOAuth2Client("ovh-eu", "aaaaaaaa", "bbbbbbbbbbbbbbbbbbbbbbbbbbbb") require.CmpNoError(err) err = client.Get("/v1/auth/time", nil) assert.String(err, "failed to retrieve OAuth2 Access Token: oauth2: cannot fetch token: 503\nResponse:

test

") } func TestOAuth2_BadJSON(t *testing.T) { assert, require := td.AssertRequire(t) httpmock.Activate() defer httpmock.DeactivateAndReset() errHTML := `

test

` httpmock.RegisterResponder("POST", "https://www.ovh.com/auth/oauth2/token", httpmock.NewStringResponder(http.StatusOK, errHTML)) // Nominal: full constructor client, err := NewOAuth2Client("ovh-eu", "aaaaaaaa", "bbbbbbbbbbbbbbbbbbbbbbbbbbbb") require.CmpNoError(err) err = client.Get("/v1/auth/time", nil) assert.String(err, "failed to retrieve OAuth2 Access Token: oauth2: cannot parse json: invalid character '<' looking for beginning of value") } func TestOAuth2_UnknownClient(t *testing.T) { assert, require := td.AssertRequire(t) httpmock.Activate() defer httpmock.DeactivateAndReset() output := `{"error":"invalid_client", "error_description":"ovhcloud oauth2 client does not exists"}` httpmock.RegisterResponder("POST", "https://www.ovh.com/auth/oauth2/token", httpmock.NewStringResponder(http.StatusBadRequest, output)) // Nominal: full constructor client, err := NewOAuth2Client("ovh-eu", "aaaaaaaa", "bbbbbbbbbbbbbbbbbbbbbbbbbbbb") require.CmpNoError(err) err = client.Get("/v1/auth/time", nil) assert.String(err, `failed to retrieve OAuth2 Access Token: oauth2: "invalid_client" "ovhcloud oauth2 client does not exists"`) } func TestOAuth2_OK(t *testing.T) { assert, require := td.AssertRequire(t) httpmock.Activate() defer httpmock.DeactivateAndReset() // expires_in set to 11 seconds. Will test that token are well renewed. // golang.org/x/oauth2 has internal 10 seconds period that it will use to renew the token before actual expiration output := `{"access_token":"cccccccccccccccc", "token_type":"Bearer", "expires_in":11,"scope":"all"}` httpmock.RegisterResponder("POST", "https://www.ovh.com/auth/oauth2/token", httpmock.NewStringResponder(http.StatusOK, output)) httpmock.RegisterResponder("GET", "https://eu.api.ovh.com/v1/auth/time", func(req *http.Request) (*http.Response, error) { assert.Cmp(req.Header.Get("Authorization"), "Bearer cccccccccccccccc") resp, err := httpmock.NewJsonResponse(http.StatusOK, map[string]string{ "hello": "world", }) require.CmpNoError(err) return resp, nil }) // Nominal: full constructor client, err := NewOAuth2Client("ovh-eu", "aaaaaaaa", "bbbbbbbbbbbbbbbbbbbbbbbbbbbb") require.CmpNoError(err) out := map[string]string{} err = client.Get("/v1/auth/time", &out) require.CmpNoError(err) assert.Cmp(out, map[string]string{ "hello": "world", }) assert.Cmp(httpmock.GetCallCountInfo(), map[string]int{ "POST https://www.ovh.com/auth/oauth2/token": 1, "GET https://eu.api.ovh.com/v1/auth/time": 1, }, "no token at this time, retrieving the token") httpmock.ZeroCallCounters() err = client.Get("/v1/auth/time", &out) require.CmpNoError(err) assert.Cmp(httpmock.GetCallCountInfo(), map[string]int{ "GET https://eu.api.ovh.com/v1/auth/time": 1, "POST https://www.ovh.com/auth/oauth2/token": 0, }, "token is still valid, no call to retrieve new token") // waiting 3 seconds, to get below the 10 seconds period time.Sleep(time.Second + 100*time.Millisecond) httpmock.ZeroCallCounters() err = client.Get("/v1/auth/time", &out) require.CmpNoError(err) assert.Cmp(httpmock.GetCallCountInfo(), map[string]int{ "GET https://eu.api.ovh.com/v1/auth/time": 1, "POST https://www.ovh.com/auth/oauth2/token": 1, }, "token is considered as expired, renewing token") } func TestOAuth2_ForReal(t *testing.T) { assert, require := td.AssertRequire(t) if os.Getenv("OAUTH2_CLIENT_ID") == "" && os.Getenv("OAUTH2_CLIENT_SECRET") == "" { t.SkipNow() } clientID, clientSecret := os.Getenv("OAUTH2_CLIENT_ID"), os.Getenv("OAUTH2_CLIENT_SECRET") // Nominal: full constructor client, err := NewOAuth2Client("ovh-eu", clientID, clientSecret) require.CmpNoError(err) type outType struct { Identities []string } out := outType{} err = client.Get("/v1/auth/details", &out) require.CmpNoError(err) require.Gte(len(out.Identities), 1) assert.Contains(out.Identities[0], "/oauth2-") assert.Contains(out.Identities[0], ":identity:credential:") } func TestSetManualEndpointValid(t *testing.T) { client := &Client{} err := client.SetEndpoint("/fake-endpoint") td.CmpNoError(t, err) td.CmpString(t, client.Endpoint(), "/fake-endpoint") } func TestSetManualEndpointInvalid(t *testing.T) { client := &Client{} err := client.SetEndpoint("/invalid-endpoint/") td.CmpString(t, err, "endpoint name cannot have a trailing slash") } go-ovh-1.9.0/ovh/testdata/000077500000000000000000000000001502677366500153465ustar00rootroot00000000000000go-ovh-1.9.0/ovh/testdata/invalid.ini000066400000000000000000000000301502677366500174660ustar00rootroot00000000000000[ovh consumer_key=local go-ovh-1.9.0/ovh/testdata/localPartial.ini000066400000000000000000000000341502677366500204530ustar00rootroot00000000000000[ovh-eu] consumer_key=local go-ovh-1.9.0/ovh/testdata/localWithURL.ini000066400000000000000000000002711502677366500203600ustar00rootroot00000000000000[ovh-eu] application_key=ovh application_secret=ovh consumer_key=ovh [https://api.example.com:4242] application_key=example.com application_secret=example.com consumer_key=example.com go-ovh-1.9.0/ovh/testdata/system.ini000066400000000000000000000001161502677366500173710ustar00rootroot00000000000000[ovh-eu] application_key=system application_secret=system consumer_key=system go-ovh-1.9.0/ovh/testdata/user.ini000066400000000000000000000001101502677366500170150ustar00rootroot00000000000000[ovh-eu] application_key=user application_secret=user consumer_key=user go-ovh-1.9.0/ovh/testdata/userPartial.ini000066400000000000000000000000631502677366500203410ustar00rootroot00000000000000[ovh-eu] application_secret=user consumer_key=user go-ovh-1.9.0/ovh/testdata/user_both.ini000066400000000000000000000001251502677366500200370ustar00rootroot00000000000000[ovh-eu] application_key=user application_secret=user client_id=foo client_secret=bargo-ovh-1.9.0/ovh/testdata/user_oauth2.ini000066400000000000000000000000501502677366500203020ustar00rootroot00000000000000[ovh-eu] client_id=foo client_secret=bargo-ovh-1.9.0/ovh/testdata/user_oauth2_incompatible.ini000066400000000000000000000000541502677366500230340ustar00rootroot00000000000000[kimsufi-eu] client_id=foo client_secret=bargo-ovh-1.9.0/ovh/testdata/user_oauth2_invalid.ini000066400000000000000000000000451502677366500220140ustar00rootroot00000000000000[ovh-eu] client_id=foo client_secret=