Add auth package

Includes AuthOpts struct and AuthRef interface, plus an Identity v2
password auth implementation.

Note: the examples work, the objectstore_test is broken, will be fixed
along with additional session and auth tests.

Change-Id: I77b07c92586c37e855b466e18dea133a4a938aaa
This commit is contained in:
Dean Troyer 2015-04-18 14:04:57 -05:00
parent ef7386d2c4
commit 9de84b3c5d
15 changed files with 551 additions and 250 deletions

View File

@ -3,18 +3,32 @@ OpenStack Golang Client
NOTE(dtroyer) Apr 2015: This repo is under heavy revision as it is being revived.
stackforge/golang-client is yet another implementation of [OpenStack]
`stackforge/golang-client` is an implementation of [OpenStack]
(http://www.openstack.org/) API client in [Go language](http://golang.org).
The code follows OpenStack licensing and borrows its infrastructure for code
hosting. It currently implements [Identity Service v2]
The code follows OpenStack licensing and uses the Stackforge infrastructure
for hosting. It currently implements [Identity Service v2]
(http://docs.openstack.org/api/openstack-identity-service/2.0/content/)
and [Object Storage v1]
(http://docs.openstack.org/api/openstack-object-storage/1.0/content/).
Some API calls are not implemented initially, but the intention is to expand
the lib over time (where pragmatic).
The initial focus is on building a solid core REST Session and OpenStack
authentication on which to build the usual API interfaces. The architecture
if the `Session` and authentication is similar to that used in the current
Python Keystone client library: The `Session` object contains the HTTP
interface methods and an authentication object that provides access to
the auth token and service catalog.
Current State
-------------
Code maturity is considered experimental.
* The new Session object is functional and used by most of the code now.
* The examples work.
* The image tests work.
* The obejct store tests do not work.
* identity/v2/auth.go is now unused, will be kept around for a short time
for easier reference.
Installation
------------
Use `go get git.openstack.org/stackforge/golang-client.git`. Or alternatively,
@ -53,7 +67,7 @@ Apache v2.
Contributing
------------
The code repository borrows OpenStack StackForge infrastructure.
The code repository utilizes the OpenStack StackForge infrastructure.
Please use the [recommended workflow]
(http://docs.openstack.org/infra/manual/developers.html#development-workflow). If you are not a member yet,
please consider joining as an [OpenStack contributor]

View File

@ -16,8 +16,10 @@ package main
import (
"fmt"
"git.openstack.org/stackforge/golang-client.git/identity/v2"
// "git.openstack.org/stackforge/golang-client.git/identity/v2"
"time"
"git.openstack.org/stackforge/golang-client.git/openstack"
)
// Authentication examples.
@ -26,42 +28,51 @@ func main() {
// Authenticate with just a username and password. The returned token is
// unscoped to a tenant.
auth, err := identity.AuthUserName(config.Host,
config.Username,
config.Password)
creds := openstack.AuthOpts{
AuthUrl: config.Host,
Username: config.Username,
Password: config.Password,
}
auth, err := openstack.DoAuthRequest(creds)
if err != nil {
fmt.Println("There was an error authenticating:", err)
fmt.Println("Error authenticating username/password:", err)
return
}
if !auth.Access.Token.Expires.After(time.Now()) {
if !auth.GetExpiration().After(time.Now()) {
fmt.Println("There was an error. The auth token has an invalid expiration.")
return
}
// Authenticate with a username, password, tenant name.
auth, err = identity.AuthUserNameTenantName(config.Host,
config.Username,
config.Password,
config.ProjectName)
// Authenticate with a project name, username, password.
creds = openstack.AuthOpts{
AuthUrl: config.Host,
Project: config.ProjectName,
Username: config.Username,
Password: config.Password,
}
auth, err = openstack.DoAuthRequest(creds)
if err != nil {
fmt.Println("There was an error authenticating:", err)
fmt.Println("Error authenticating project/username/password:", err)
return
}
if !auth.Access.Token.Expires.After(time.Now()) {
if !auth.GetExpiration().After(time.Now()) {
fmt.Println("There was an error. The auth token has an invalid expiration.")
return
}
// Authenticate with a username, password, tenant id.
auth, err = identity.AuthUserNameTenantId(config.Host,
config.Username,
config.Password,
config.ProjectID)
// Authenticate with a project id, username, password.
creds = openstack.AuthOpts{
AuthUrl: config.Host,
Project: config.ProjectID,
Username: config.Username,
Password: config.Password,
}
auth, err = openstack.DoAuthRequest(creds)
if err != nil {
fmt.Println("There was an error authenticating:", err)
fmt.Println("Error authenticating project/username/password:", err)
return
}
if !auth.Access.Token.Expires.After(time.Now()) {
if !auth.GetExpiration().After(time.Now()) {
fmt.Println("There was an error. The auth token has an invalid expiration.")
return
}

View File

@ -18,10 +18,12 @@ import (
"bytes"
"encoding/json"
"fmt"
"git.openstack.org/stackforge/golang-client.git/identity/v2"
"git.openstack.org/stackforge/golang-client.git/objectstorage/v1"
"io/ioutil"
"net/http"
"time"
"git.openstack.org/stackforge/golang-client.git/objectstorage/v1"
"git.openstack.org/stackforge/golang-client.git/openstack"
)
func main() {
@ -29,45 +31,52 @@ func main() {
// Before working with object storage we need to authenticate with a project
// that has active object storage.
auth, err := identity.AuthUserNameTenantName(config.Host,
config.Username,
config.Password,
config.ProjectName)
// Authenticate with a project name, username, password.
creds := openstack.AuthOpts{
AuthUrl: config.Host,
Project: config.ProjectName,
Username: config.Username,
Password: config.Password,
}
auth, err := openstack.DoAuthRequest(creds)
if err != nil {
panicString := fmt.Sprint("There was an error authenticating:", err)
panic(panicString)
}
if !auth.Access.Token.Expires.After(time.Now()) {
if !auth.GetExpiration().After(time.Now()) {
panic("There was an error. The auth token has an invalid expiration.")
}
// Find the endpoint for object storage.
url := ""
for _, svc := range auth.Access.ServiceCatalog {
if svc.Type == "object-store" {
url = svc.Endpoints[0].PublicURL + "/"
break
}
}
if url == "" {
url, err := auth.GetEndpoint("object-store", "")
if url == "" || err != nil {
panic("object-store url not found during authentication")
}
hdr, err := objectstorage.GetAccountMeta(url, auth.Access.Token.Id)
// Make a new client with these creds
sess, err := openstack.NewSession(nil, auth, nil)
if err != nil {
panicString := fmt.Sprint("Error crating new Session:", err)
panic(panicString)
}
hdr, err := objectstorage.GetAccountMeta(sess, url)
if err != nil {
panicString := fmt.Sprint("There was an error getting account metadata:", err)
panic(panicString)
}
_ = hdr
// Create a new container.
if err = objectstorage.PutContainer(url+config.Container, auth.Access.Token.Id,
"X-Log-Retention", "true"); err != nil {
var headers http.Header = http.Header{}
headers.Add("X-Log-Retention", "true")
if err = objectstorage.PutContainer(sess, url+"/"+config.Container, headers); err != nil {
panicString := fmt.Sprint("PutContainer Error:", err)
panic(panicString)
}
// Get a list of all the containers at the selected endoint.
containersJson, err := objectstorage.ListContainers(0, "", url, auth.Access.Token.Id)
containersJson, err := objectstorage.ListContainers(sess, 0, "", url)
if err != nil {
panic(err)
}
@ -93,12 +102,13 @@ func main() {
}
// Set and Get container metadata.
if err = objectstorage.SetContainerMeta(url+config.Container, auth.Access.Token.Id,
"X-Container-Meta-fubar", "false"); err != nil {
headers = http.Header{}
headers.Add("X-Container-Meta-fubar", "false")
if err = objectstorage.SetContainerMeta(sess, url+"/"+config.Container, headers); err != nil {
panic(err)
}
hdr, err = objectstorage.GetContainerMeta(url+config.Container, auth.Access.Token.Id)
hdr, err = objectstorage.GetContainerMeta(sess, url+"/"+config.Container)
if err != nil {
panicString := fmt.Sprint("GetContainerMeta Error:", err)
panic(panicString)
@ -115,13 +125,14 @@ func main() {
panic(err)
}
headers = http.Header{}
headers.Add("X-Container-Meta-fubar", "false")
object := config.Container + "/" + srcFile
if err = objectstorage.PutObject(&fContent, url+object, auth.Access.Token.Id,
"X-Object-Meta-fubar", "false"); err != nil {
if err = objectstorage.PutObject(sess, &fContent, url+"/"+object, headers); err != nil {
panic(err)
}
objectsJson, err := objectstorage.ListObjects(0, "", "", "", "",
url+config.Container, auth.Access.Token.Id)
objectsJson, err := objectstorage.ListObjects(sess, 0, "", "", "", "",
url+"/"+config.Container)
type objectType struct {
Name, Hash, Content_type, Last_modified string
@ -143,12 +154,13 @@ func main() {
}
// Manage object metadata
if err = objectstorage.SetObjectMeta(url+object, auth.Access.Token.Id,
"X-Object-Meta-fubar", "true"); err != nil {
headers = http.Header{}
headers.Add("X-Object-Meta-fubar", "true")
if err = objectstorage.SetObjectMeta(sess, url+"/"+object, headers); err != nil {
panicString := fmt.Sprint("SetObjectMeta Error:", err)
panic(panicString)
}
hdr, err = objectstorage.GetObjectMeta(url+object, auth.Access.Token.Id)
hdr, err = objectstorage.GetObjectMeta(sess, url+"/"+object)
if err != nil {
panicString := fmt.Sprint("GetObjectMeta Error:", err)
panic(panicString)
@ -159,7 +171,7 @@ func main() {
}
// Retrieve an object and check that it is the same as what as uploaded.
_, body, err := objectstorage.GetObject(url+object, auth.Access.Token.Id)
_, body, err := objectstorage.GetObject(sess, url+"/"+object)
if err != nil {
panicString := fmt.Sprint("GetObject Error:", err)
panic(panicString)
@ -170,24 +182,23 @@ func main() {
}
// Duplication (Copy) an existing object.
if err = objectstorage.CopyObject(url+object, "/"+object+".dup", auth.Access.Token.Id); err != nil {
if err = objectstorage.CopyObject(sess, url+"/"+object, "/"+object+".dup"); err != nil {
panicString := fmt.Sprint("CopyObject Error:", err)
panic(panicString)
}
// Delete the objects.
if err = objectstorage.DeleteObject(url+object, auth.Access.Token.Id); err != nil {
if err = objectstorage.DeleteObject(sess, url+"/"+object); err != nil {
panicString := fmt.Sprint("DeleteObject Error:", err)
panic(panicString)
}
if err = objectstorage.DeleteObject(url+object+".dup", auth.Access.Token.Id); err != nil {
if err = objectstorage.DeleteObject(sess, url+"/"+object+".dup"); err != nil {
panicString := fmt.Sprint("DeleteObject Error:", err)
panic(panicString)
}
// Delete the container that was previously created.
if err = objectstorage.DeleteContainer(url+config.Container,
auth.Access.Token.Id); err != nil {
if err = objectstorage.DeleteContainer(sess, url+"/"+config.Container); err != nil {
panicString := fmt.Sprint("DeleteContainer Error:", err)
panic(panicString)
}

View File

@ -16,10 +16,11 @@ package main
import (
"fmt"
"git.openstack.org/stackforge/golang-client.git/identity/v2"
"git.openstack.org/stackforge/golang-client.git/image/v1"
"net/http"
"time"
"git.openstack.org/stackforge/golang-client.git/image/v1"
"git.openstack.org/stackforge/golang-client.git/openstack"
)
// Image examples.
@ -27,34 +28,39 @@ func main() {
config := getConfig()
// Authenticate with a username, password, tenant id.
auth, err := identity.AuthUserNameTenantName(config.Host,
config.Username,
config.Password,
config.ProjectName)
creds := openstack.AuthOpts{
AuthUrl: config.Host,
Project: config.ProjectName,
Username: config.Username,
Password: config.Password,
}
auth, err := openstack.DoAuthRequest(creds)
if err != nil {
panicString := fmt.Sprint("There was an error authenticating:", err)
panic(panicString)
}
if !auth.Access.Token.Expires.After(time.Now()) {
if !auth.GetExpiration().After(time.Now()) {
panic("There was an error. The auth token has an invalid expiration.")
}
// Find the endpoint for the image service.
url := ""
for _, svc := range auth.Access.ServiceCatalog {
if svc.Type == "image" {
for _, ep := range svc.Endpoints {
url = ep.PublicURL + "/v1"
break
}
}
}
if url == "" {
url, err := auth.GetEndpoint("image", "")
if url == "" || err != nil {
panic("v1 image service url not found during authentication")
}
imageService := image.Service{TokenID: auth.Access.Token.Id, Client: *http.DefaultClient, URL: url}
// Make a new client with these creds
sess, err := openstack.NewSession(nil, auth, nil)
if err != nil {
panicString := fmt.Sprint("Error crating new Session:", err)
panic(panicString)
}
imageService := image.Service{
Session: *sess,
Client: *http.DefaultClient,
URL: url + "/v1", // We're forcing Image v1 for now
}
imagesDetails, err := imageService.ImagesDetail()
if err != nil {
panicString := fmt.Sprint("Cannot access images:", err)

View File

@ -24,7 +24,9 @@ package image
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
@ -38,8 +40,8 @@ import (
// imageService := image.ImageService{Client: *http.DefaultClient, TokenId: tokenId, Url: "http://imageservicelocation"}
// images:= imageService.Images()
type Service struct {
Session openstack.Session
Client http.Client
TokenID string
URL string
}
@ -150,19 +152,22 @@ func (imageService Service) queryImages(includeDetails bool, imagesResponseConta
}
var headers http.Header = http.Header{}
headers.Set("X-Auth-Token", imageService.TokenID)
headers.Set("Accept", "application/json")
resp, err := openstack.Get(reqURL.String(), nil, &headers)
resp, err := imageService.Session.Get(reqURL.String(), nil, &headers)
if err != nil {
return err
}
err = util.CheckHTTPResponseStatusCode(resp.Resp)
err = util.CheckHTTPResponseStatusCode(resp)
if err != nil {
return err
}
if err = json.Unmarshal(resp.Body, &imagesResponseContainer); err != nil {
rbody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return errors.New("aaa")
}
if err = json.Unmarshal(rbody, &imagesResponseContainer); err != nil {
return err
}
return nil

View File

@ -22,6 +22,7 @@ import (
"testing"
"git.openstack.org/stackforge/golang-client.git/image/v1"
"git.openstack.org/stackforge/golang-client.git/openstack"
"git.openstack.org/stackforge/golang-client.git/testUtil"
"git.openstack.org/stackforge/golang-client.git/util"
)
@ -159,7 +160,18 @@ func testImageServiceAction(t *testing.T, uriEndsWith string, testData string, i
apiServer := testUtil.CreateGetJSONTestRequestServer(t, tokn, testData, anon)
defer apiServer.Close()
imageService := image.Service{TokenID: tokn, Client: *http.DefaultClient, URL: apiServer.URL}
auth := openstack.AuthToken{
Access: openstack.AccessType{
Token: openstack.Token{
ID: tokn,
},
},
}
sess, _ := openstack.NewSession(http.DefaultClient, auth, nil)
imageService := image.Service{
Session: *sess,
URL: apiServer.URL,
}
imageServiceAction(&imageService)
}

View File

@ -31,65 +31,65 @@ var zeroByte = &([]byte{}) //pointer to empty []byte
//"limit" and "marker" corresponds to the API's "limit" and "marker".
//"url" can be regular storage or cdn-enabled storage URL.
//It returns []byte which then needs to be unmarshalled to decode the JSON.
func ListContainers(limit int64, marker, url, token string) ([]byte, error) {
return ListObjects(limit, marker, "", "", "", url, token)
func ListContainers(session *openstack.Session, limit int64, marker, url string) ([]byte, error) {
return ListObjects(session, limit, marker, "", "", "", url)
}
//GetAccountMeta calls the OpenStack retrieve account metadata API using
//previously obtained token.
func GetAccountMeta(url, token string) (http.Header, error) {
return GetObjectMeta(url, token)
func GetAccountMeta(session *openstack.Session, url string) (http.Header, error) {
return GetObjectMeta(session, url)
}
//DeleteContainer calls the OpenStack delete container API using
//previously obtained token.
func DeleteContainer(url, token string) error {
return DeleteObject(url, token)
func DeleteContainer(session *openstack.Session, url string) error {
return DeleteObject(session, url)
}
//GetContainerMeta calls the OpenStack retrieve object metadata API
//using previously obtained token.
//url can be regular storage or CDN-enabled storage URL.
func GetContainerMeta(url, token string) (http.Header, error) {
return GetObjectMeta(url, token)
func GetContainerMeta(session *openstack.Session, url string) (http.Header, error) {
return GetObjectMeta(session, url)
}
//SetContainerMeta calls the OpenStack API to create / update meta data
//for container using previously obtained token.
//url can be regular storage or CDN-enabled storage URL.
func SetContainerMeta(url string, token string, s ...string) (err error) {
return SetObjectMeta(url, token, s...)
func SetContainerMeta(session *openstack.Session, url string, headers http.Header) (err error) {
return SetObjectMeta(session, url, headers)
}
//PutContainer calls the OpenStack API to create / update
//container using previously obtained token.
func PutContainer(url, token string, s ...string) error {
return PutObject(zeroByte, url, token, s...)
func PutContainer(session *openstack.Session, url string, headers http.Header) error {
return PutObject(session, zeroByte, url, headers)
}
//ListObjects calls the OpenStack list object API using previously
//obtained token. "Limit", "marker", "prefix", "path", "delim" corresponds
//to the API's "limit", "marker", "prefix", "path", and "delimiter".
func ListObjects(limit int64,
marker, prefix, path, delim, conURL, token string) ([]byte, error) {
var query = "?format=json"
func ListObjects(session *openstack.Session, limit int64,
marker, prefix, path, delim, conURL string) ([]byte, error) {
var query url.Values = url.Values{}
query.Add("format", "json")
if limit > 0 {
query += "&limit=" + strconv.FormatInt(limit, 10)
query.Add("limit", strconv.FormatInt(limit, 10))
}
if marker != "" {
query += "&marker=" + url.QueryEscape(marker)
query.Add("marker", url.QueryEscape(marker))
}
if prefix != "" {
query += "&prefix=" + url.QueryEscape(prefix)
query.Add("prefix", url.QueryEscape(prefix))
}
if path != "" {
query += "&path=" + url.QueryEscape(path)
query.Add("path", url.QueryEscape(path))
}
if delim != "" {
query += "&delimiter=" + url.QueryEscape(delim)
query.Add("delimiter", url.QueryEscape(delim))
}
resp, err := util.CallAPI("GET", conURL+query, zeroByte,
"X-Auth-Token", token)
resp, err := session.Get(conURL, &query, nil)
if err != nil {
return nil, err
}
@ -107,23 +107,21 @@ func ListObjects(limit int64,
//PutObject calls the OpenStack create object API using previously
//obtained token.
//url can be regular storage or CDN-enabled storage URL.
func PutObject(fContent *[]byte, url, token string, s ...string) (err error) {
var headers http.Header = http.Header{}
headers.Set("X-Auth-Token", token)
resp, err := openstack.Put(url, nil, &headers, fContent)
func PutObject(session *openstack.Session, fContent *[]byte, url string, headers http.Header) (err error) {
resp, err := session.Put(url, nil, &headers, fContent)
if err != nil {
return err
}
return util.CheckHTTPResponseStatusCode(resp.Resp)
return util.CheckHTTPResponseStatusCode(resp)
}
//CopyObject calls the OpenStack copy object API using previously obtained
//token. Note from API doc: "The destination container must exist before
//attempting the copy."
func CopyObject(srcURL, destURL, token string) (err error) {
resp, err := util.CallAPI("COPY", srcURL, zeroByte,
"X-Auth-Token", token,
"Destination", destURL)
func CopyObject(session *openstack.Session, srcURL, destURL string) (err error) {
var headers http.Header = http.Header{}
headers.Add("Destination", destURL)
resp, err := session.Request("COPY", srcURL, nil, &headers, zeroByte)
if err != nil {
return err
}
@ -138,8 +136,8 @@ func CopyObject(srcURL, destURL, token string) (err error) {
//from the non-current container to the current." .. "If you want to completely
//remove an object and you have five total versions of it, you must DELETE it
//five times."
func DeleteObject(url, token string) (err error) {
resp, err := util.CallAPI("DELETE", url, zeroByte, "X-Auth-Token", token)
func DeleteObject(session *openstack.Session, url string) (err error) {
resp, err := session.Delete(url, nil, nil)
if err != nil {
return err
}
@ -148,10 +146,9 @@ func DeleteObject(url, token string) (err error) {
//SetObjectMeta calls the OpenStack API to create/update meta data for
//object using previously obtained token.
func SetObjectMeta(url string, token string, s ...string) (err error) {
s = append(s, "X-Auth-Token")
s = append(s, token)
resp, err := util.CallAPI("POST", url, zeroByte, s...)
func SetObjectMeta(session *openstack.Session, url string, headers http.Header) (err error) {
// headers.Add("X-Auth-Token", token)
resp, err := session.Post(url, nil, &headers, zeroByte)
if err != nil {
return err
}
@ -160,8 +157,8 @@ func SetObjectMeta(url string, token string, s ...string) (err error) {
//GetObjectMeta calls the OpenStack retrieve object metadata API using
//previously obtained token.
func GetObjectMeta(url, token string) (http.Header, error) {
resp, err := util.CallAPI("HEAD", url, zeroByte, "X-Auth-Token", token)
func GetObjectMeta(session *openstack.Session, url string) (http.Header, error) {
resp, err := session.Head(url, nil, nil)
if err != nil {
return nil, err
}
@ -175,8 +172,8 @@ func GetObjectMeta(url, token string) (http.Header, error) {
//Since this implementation of GetObject retrieves header info, it
//effectively executes GetObjectMeta also in addition to getting the
//object content.
func GetObject(url, token string) (http.Header, []byte, error) {
resp, err := util.CallAPI("GET", url, zeroByte, "X-Auth-Token", token)
func GetObject(session *openstack.Session, url string) (http.Header, []byte, error) {
resp, err := session.Get(url, nil, nil)
if err != nil {
return nil, nil, err
}

View File

@ -2,3 +2,12 @@ openstack
=========
`openstack` is the API to an OpenStack cloud.
* `session.go` - A Session object that encapsulates the HTTP REST handler
and authentication and logging
* `auth.go` - The basic authentication interface
* `auth-password.go` - Implements password authentication (v2 only at present)
* `auth-token.go` - The returned token objects

View File

@ -0,0 +1,81 @@
// auth-password - Username/Password Authentication
// Copyright 2015 Dean Troyer
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package openstack
import (
"encoding/json"
"errors"
// "strings"
)
// The token request structure for Identity v2
type PasswordCredentials struct {
Username string `json:"username"`
Password string `json:"password"`
}
type OSAuth struct {
PasswordCredentials `json:"passwordCredentials"`
Project string `json:"tenantName"`
}
type UserPassV2 struct {
OSAuth `json:"auth"`
AuthUrl string `json:"-"`
}
func NewUserPassV2(ao AuthOpts) (upv2 *UserPassV2, err error) {
// Validate incoming values
if ao.AuthUrl == "" {
err = errors.New("AuthUrl required")
return nil, err
}
if ao.Username == "" {
err = errors.New("Username required")
return nil, err
}
if ao.Password == "" {
err = errors.New("Password required")
return nil, err
}
upv2 = &UserPassV2{
AuthUrl: ao.AuthUrl,
OSAuth: OSAuth{
PasswordCredentials: PasswordCredentials{
Username: ao.Username,
Password: ao.Password,
},
Project: ao.Project,
},
}
return upv2, nil
}
// Produce JSON output
func (s *UserPassV2) JSON() []byte {
reqAuth, err := json.Marshal(s)
if err != nil {
// Return an empty structure
reqAuth = []byte{'{', '}'}
}
return reqAuth
}
// func (self *UserPassV2) AuthUserPassV2(opts interface{}) (AuthRef, error) {
// auth, err := self.GetAuthRef()
// return AuthRef(auth), err
// }

81
openstack/auth-token.go Normal file
View File

@ -0,0 +1,81 @@
// auth-token - Token Authentication
// Copyright 2015 Dean Troyer
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package openstack
import (
"errors"
"time"
)
// Identity Response Types
type AccessType struct {
Token Token `json:"token"`
User interface{} `json:"id"`
ServiceCatalog []ServiceCatalogEntry `json:"servicecatalog"`
}
type AuthToken struct {
Access AccessType `json:"access"`
}
type Token struct {
ID string `json:"id"`
Expires time.Time `json:"expires"`
Project struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"tenant"`
}
type ServiceCatalogEntry struct {
Name string `json:"name"`
Type string `json:"type"`
Endpoints []ServiceEndpoint `json:"endpoints"`
// Endpoints []map[string]string `json:"endpoints"`
}
type ServiceEndpoint struct {
Type string `json:"type"`
Region string `json:"region"`
PublicURL string `json:"publicurl"`
AdminURL string `json:"adminurl"`
InternalURL string `json:"internalurl"`
VersionID string `json:"versionid"`
}
func (s AuthToken) GetToken() string {
return s.Access.Token.ID
}
func (s AuthToken) GetExpiration() time.Time {
return s.Access.Token.Expires
}
func (s AuthToken) GetEndpoint(serviceType string, regionName string) (string, error) {
// Parse service catalog
for _, v := range s.Access.ServiceCatalog {
if v.Type == serviceType {
for _, r := range v.Endpoints {
if regionName == "" || r.Region == regionName {
return r.PublicURL, nil
}
}
}
}
return "", errors.New("err: endpoint not found")
}

99
openstack/auth.go Normal file
View File

@ -0,0 +1,99 @@
// auth - Authentication interface
// Copyright 2015 Dean Troyer
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package openstack
import (
"encoding/json"
"errors"
"io/ioutil"
"strings"
"time"
)
// AuthRef is the returned authentication object, maybe v2 or v3
type AuthRef interface {
GetToken() string
GetExpiration() time.Time
GetEndpoint(string, string) (string, error)
}
// AuthOpts is the set of credentials used to authenticate to OpenStack
type AuthOpts struct {
// AuthUrl is always required
AuthUrl string
// Domain is ignored for v2 and required for v3 auth
Domain string
// Project is optional to get an unscoped token but required for
// a scoped token, which is required to do pretty much everything
// except list projects
Project string
// Username is required for password auth
Username string
// Password is required for password auth
Password string
// Token is required for Toekn auth
Token string
}
func (s *AuthOpts) GetAuthType() (string, error) {
var auth_type string
if s.AuthUrl != "" && s.Token != "" {
auth_type = "token"
} else if s.Username != "" {
auth_type = "password"
}
return auth_type, nil
}
// Basic auth call
// These args should be an interface??
func DoAuthRequest(authopts AuthOpts) (AuthRef, error) {
// url string, body []byte)
var auth = AuthToken{}
auth_mod, err := NewUserPassV2(authopts)
if err != nil {
err = errors.New("Failed to get auth options")
return nil, err
}
path := auth_mod.AuthUrl + "/tokens"
body := auth_mod.JSON()
resp, err := Post(path, nil, nil, &body)
if err != nil {
return nil, err
}
contentType := strings.ToLower(resp.Header.Get("Content-Type"))
if strings.Contains(contentType, "json") != true {
return nil, errors.New("err: header Content-Type is not JSON")
}
rbody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, errors.New("aaa")
}
if err = json.Unmarshal(rbody, &auth); err != nil {
return nil, errors.New("bbb")
}
return auth, nil
}

View File

@ -19,12 +19,11 @@ import (
"bytes"
"crypto/tls"
"io"
"io/ioutil"
// "io/ioutil"
"log"
"net/http"
"net/http/httputil"
"net/url"
"strings"
)
var Debug = new(bool)
@ -34,46 +33,28 @@ type Response struct {
Body []byte
}
type TokenInterface interface {
GetTokenId() string
}
type Token struct {
Expires string
Id string
Project struct {
Id string
Name string
}
}
func (t Token) GetTokenId() string {
return t.Id
}
// Generic callback to get a token from the auth plugin
type AuthFunc func(s *Session, opts interface{}) (TokenInterface, error)
type AuthFunc func(s *Session, opts interface{}) (AuthRef, error)
type Session struct {
httpClient *http.Client
endpoint string
authenticate AuthFunc
Token TokenInterface
Headers http.Header
// ServCat map[string]ServiceEndpoint
httpClient *http.Client
AuthToken AuthRef
Headers http.Header
}
func NewSession(af AuthFunc, endpoint string, tls *tls.Config) (session *Session, err error) {
tr := &http.Transport{
TLSClientConfig: tls,
DisableCompression: true,
func NewSession(hclient *http.Client, auth AuthRef, tls *tls.Config) (session *Session, err error) {
if hclient == nil {
// Only build a transport if we're also building the client
tr := &http.Transport{
TLSClientConfig: tls,
DisableCompression: true,
}
hclient = &http.Client{Transport: tr}
}
session = &Session{
// TODO(dtroyer): httpClient needs to be able to be passed in, or set externally
httpClient: &http.Client{Transport: tr},
endpoint: strings.TrimRight(endpoint, "/"),
authenticate: af,
Headers: http.Header{},
httpClient: hclient,
AuthToken: auth,
Headers: http.Header{},
}
return session, nil
}
@ -83,47 +64,35 @@ func (s *Session) NewRequest(method, url string, headers *http.Header, body io.R
if err != nil {
return nil, err
}
// add token, get one if needed
if s.Token == nil && s.authenticate != nil {
var tok TokenInterface
tok, err = s.authenticate(s, nil)
if err != nil {
// (re-)auth failure!!
return nil, err
}
s.Token = tok
}
if headers != nil {
req.Header = *headers
}
if s.Token != nil {
req.Header.Add("X-Auth-Token", s.Token.GetTokenId())
if s.AuthToken != nil {
req.Header.Add("X-Auth-Token", s.AuthToken.GetToken())
}
return
}
func (s *Session) Do(req *http.Request) (*Response, error) {
if *Debug {
d, _ := httputil.DumpRequestOut(req, true)
log.Printf(">>>>>>>>>> REQUEST:\n", string(d))
}
func (s *Session) Do(req *http.Request) (*http.Response, error) {
// Add session headers
for k := range s.Headers {
req.Header.Set(k, s.Headers.Get(k))
}
hresp, err := s.httpClient.Do(req)
if *Debug {
d, _ := httputil.DumpRequestOut(req, true)
log.Printf(">>>>>>>>>> REQUEST:\n", string(d))
}
resp, err := s.httpClient.Do(req)
if err != nil {
return nil, err
}
if *Debug {
dr, _ := httputil.DumpResponse(hresp, true)
dr, _ := httputil.DumpResponse(resp, true)
log.Printf("<<<<<<<<<< RESULT:\n", string(dr))
}
resp := new(Response)
resp.Resp = hresp
return resp, nil
}
@ -134,7 +103,7 @@ func (s *Session) Request(
params *url.Values,
headers *http.Header,
body *[]byte,
) (resp *Response, err error) {
) (resp *http.Response, err error) {
// add params to url here
if params != nil {
url = url + "?" + params.Encode()
@ -151,35 +120,42 @@ func (s *Session) Request(
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
resp, err = s.Do(req)
if err != nil {
return nil, err
}
// do we need to parse this in this func? yes...
defer resp.Resp.Body.Close()
resp.Body, err = ioutil.ReadAll(resp.Resp.Body)
if err != nil {
return nil, err
}
return resp, nil
}
func (s *Session) Delete(
url string,
params *url.Values,
headers *http.Header) (resp *http.Response, err error) {
return s.Request("DELETE", url, params, headers, nil)
}
func (s *Session) Get(
url string,
params *url.Values,
headers *http.Header) (resp *Response, err error) {
headers *http.Header) (resp *http.Response, err error) {
return s.Request("GET", url, params, headers, nil)
}
func (s *Session) Head(
url string,
params *url.Values,
headers *http.Header) (resp *http.Response, err error) {
return s.Request("HEAD", url, params, headers, nil)
}
func (s *Session) Post(
url string,
params *url.Values,
headers *http.Header,
body *[]byte) (resp *Response, err error) {
body *[]byte) (resp *http.Response, err error) {
return s.Request("POST", url, params, headers, body)
}
@ -187,7 +163,7 @@ func (s *Session) Put(
url string,
params *url.Values,
headers *http.Header,
body *[]byte) (resp *Response, err error) {
body *[]byte) (resp *http.Response, err error) {
return s.Request("PUT", url, params, headers, body)
}
@ -195,8 +171,8 @@ func (s *Session) Put(
func Get(
url string,
params *url.Values,
headers *http.Header) (resp *Response, err error) {
s, _ := NewSession(nil, "", nil)
headers *http.Header) (resp *http.Response, err error) {
s, _ := NewSession(nil, nil, nil)
return s.Get(url, params, headers)
}
@ -205,8 +181,8 @@ func Post(
url string,
params *url.Values,
headers *http.Header,
body *[]byte) (resp *Response, err error) {
s, _ := NewSession(nil, "", nil)
body *[]byte) (resp *http.Response, err error) {
s, _ := NewSession(nil, nil, nil)
return s.Post(url, params, headers, body)
}
@ -215,7 +191,7 @@ func Put(
url string,
params *url.Values,
headers *http.Header,
body *[]byte) (resp *Response, err error) {
s, _ := NewSession(nil, "", nil)
body *[]byte) (resp *http.Response, err error) {
s, _ := NewSession(nil, nil, nil)
return s.Put(url, params, headers, body)
}

View File

@ -13,48 +13,47 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package openstack_test
import (
"encoding/json"
"net/http"
"testing"
"encoding/json"
"net/http"
"testing"
"git.openstack.org/stackforge/golang-client.git/openstack"
"git.openstack.org/stackforge/golang-client.git/testUtil"
"git.openstack.org/stackforge/golang-client.git/openstack"
"git.openstack.org/stackforge/golang-client.git/testUtil"
)
type TestStruct struct {
ID string `json:"id"`
Name string `json:"name"`
ID string `json:"id"`
Name string `json:"name"`
}
func TestSessionGet(t *testing.T) {
tokn := "eaaafd18-0fed-4b3a-81b4-663c99ec1cbb"
var apiServer = testUtil.CreateGetJsonTestServer(
t,
tokn,
`{"id":"id1","name":"Chris"}`,
nil,
)
expected := TestStruct{ID: "id1", Name: "Chris"}
actual := TestStruct{}
tokn := "eaaafd18-0fed-4b3a-81b4-663c99ec1cbb"
var apiServer = testUtil.CreateGetJsonTestServer(
t,
tokn,
`{"id":"id1","name":"Chris"}`,
nil,
)
expected := TestStruct{ID: "id1", Name: "Chris"}
actual := TestStruct{}
s, _ := openstack.NewSession(nil, "", nil)
var headers http.Header = http.Header{}
headers.Set("X-Auth-Token", tokn)
headers.Set("Accept", "application/json")
headers.Set("Etag", "md5hash-blahblah")
resp, err := s.Get(apiServer.URL, nil, &headers)
if err != nil {
t.Error(err)
}
testUtil.IsNil(t, err)
s, _ := openstack.NewSession(nil, "", nil)
var headers http.Header = http.Header{}
headers.Set("X-Auth-Token", tokn)
headers.Set("Accept", "application/json")
headers.Set("Etag", "md5hash-blahblah")
resp, err := s.Get(apiServer.URL, nil, &headers)
if err != nil {
t.Error(err)
}
testUtil.IsNil(t, err)
if err = json.Unmarshal(resp.Body, &actual); err != nil {
t.Error(err)
}
if err = json.Unmarshal(resp.Body, &actual); err != nil {
t.Error(err)
}
testUtil.Equals(t, expected, actual)
testUtil.Equals(t, expected, actual)
}

View File

@ -61,24 +61,24 @@ func IsNil(tb testing.TB, act interface{}) {
// JSON Get requests. Takes a token, JSON payload, and a verification function
// to do additional validation
func CreateGetJsonTestServer(
t *testing.T,
expectedAuthToken string,
jsonResponsePayload string,
verifyRequest func(*http.Request)) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
headerValuesEqual(t, r, "X-Auth-Token", expectedAuthToken)
headerValuesEqual(t, r, "Accept", "application/json")
// verifyRequest(r)
if r.Method == "GET" {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(jsonResponsePayload))
w.WriteHeader(http.StatusOK)
return
}
t *testing.T,
expectedAuthToken string,
jsonResponsePayload string,
verifyRequest func(*http.Request)) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
headerValuesEqual(t, r, "X-Auth-Token", expectedAuthToken)
headerValuesEqual(t, r, "Accept", "application/json")
// verifyRequest(r)
if r.Method == "GET" {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(jsonResponsePayload))
w.WriteHeader(http.StatusOK)
return
}
t.Error(errors.New("Failed: r.Method == GET"))
}))
t.Error(errors.New("Failed: r.Method == GET"))
}))
}
// CreateGetJSONTestRequestServer creates a httptest.Server that can be used to test GetJson requests. Just specify the token,