openstack: implement account key for cluster-internal authentication

This commit is contained in:
Malte Poll 2023-03-17 09:26:29 +01:00 committed by Malte Poll
parent 1b2a927b84
commit f785ae560b
3 changed files with 322 additions and 0 deletions

View File

@ -4,6 +4,7 @@ load("//bazel/go:go_test.bzl", "go_test")
go_library(
name = "openstack",
srcs = [
"accountkey.go",
"api.go",
"imds.go",
"openstack.go",
@ -27,6 +28,7 @@ go_library(
go_test(
name = "openstack_test",
srcs = [
"accountkey_test.go",
"api_test.go",
"imds_test.go",
"openstack_test.go",

View File

@ -0,0 +1,145 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package openstack
import (
"fmt"
"net/url"
"regexp"
)
// AccountKey is a OpenStack account key.
type AccountKey struct {
AuthURL string
Username string
Password string
ProjectID string
ProjectName string
UserDomainName string
ProjectDomainName string
RegionName string
}
// AccountKeyFromURI parses ServiceAccountKey from URI.
func AccountKeyFromURI(serviceAccountURI string) (AccountKey, error) {
uri, err := url.Parse(serviceAccountURI)
if err != nil {
return AccountKey{}, err
}
if uri.Scheme != "serviceaccount" {
return AccountKey{}, fmt.Errorf("invalid service account URI: invalid scheme: %s", uri.Scheme)
}
if uri.Host != "openstack" {
return AccountKey{}, fmt.Errorf("invalid service account URI: invalid host: %s", uri.Host)
}
query := uri.Query()
if query.Get("auth_url") == "" {
return AccountKey{}, fmt.Errorf("invalid service account URI: missing parameter \"auth_url\": %s", uri)
}
if query.Get("username") == "" {
return AccountKey{}, fmt.Errorf("invalid service account URI: missing parameter \"username\": %s", uri)
}
if query.Get("password") == "" {
return AccountKey{}, fmt.Errorf("invalid service account URI: missing parameter \"password\": %s", uri)
}
if query.Get("project_id") == "" {
return AccountKey{}, fmt.Errorf("invalid service account URI: missing parameter \"project_id\": %s", uri)
}
if query.Get("project_name") == "" {
return AccountKey{}, fmt.Errorf("invalid service account URI: missing parameter \"project_name\": %s", uri)
}
if query.Get("user_domain_name") == "" {
return AccountKey{}, fmt.Errorf("invalid service account URI: missing parameter \"user_domain_name\": %s", uri)
}
if query.Get("project_domain_name") == "" {
return AccountKey{}, fmt.Errorf("invalid service account URI: missing parameter \"project_domain_name\": %s", uri)
}
if query.Get("region_name") == "" {
return AccountKey{}, fmt.Errorf("invalid service account URI: missing parameter \"region_name\": %s", uri)
}
return AccountKey{
AuthURL: query.Get("auth_url"),
Username: query.Get("username"),
Password: query.Get("password"),
ProjectID: query.Get("project_id"),
ProjectName: query.Get("project_name"),
UserDomainName: query.Get("user_domain_name"),
ProjectDomainName: query.Get("project_domain_name"),
RegionName: query.Get("region_name"),
}, nil
}
// ToCloudServiceAccountURI converts the AccountKey into a cloud service account URI.
func (k AccountKey) ToCloudServiceAccountURI() string {
query := url.Values{}
query.Add("auth_url", k.AuthURL)
query.Add("username", k.Username)
query.Add("password", k.Password)
query.Add("project_id", k.ProjectID)
query.Add("project_name", k.ProjectName)
query.Add("user_domain_name", k.UserDomainName)
query.Add("project_domain_name", k.ProjectDomainName)
query.Add("region_name", k.RegionName)
uri := url.URL{
Scheme: "serviceaccount",
Host: "openstack",
RawQuery: query.Encode(),
}
return uri.String()
}
// CloudINI converts the AccountKey into a CloudINI.
func (k AccountKey) CloudINI() CloudINI {
return CloudINI{
AuthURL: k.AuthURL,
Username: k.Username,
Password: k.Password,
TenantID: k.ProjectID,
TenantName: k.ProjectName,
UserDomainName: k.UserDomainName,
TenantDomainName: k.ProjectDomainName,
Region: k.RegionName,
}
}
// CloudINI is a struct that represents the cloud.ini file used by OpenStack k8s deployments.
type CloudINI struct {
AuthURL string `gcfg:"auth-url" mapstructure:"auth-url" name:"os-authURL" dependsOn:"os-password|os-trustID|os-applicationCredentialSecret|os-clientCertPath"`
Username string `name:"os-userName" value:"optional" dependsOn:"os-password"`
Password string `name:"os-password" value:"optional" dependsOn:"os-domainID|os-domainName,os-projectID|os-projectName,os-userID|os-userName"`
TenantID string `gcfg:"tenant-id" mapstructure:"project-id" name:"os-projectID" value:"optional" dependsOn:"os-password|os-clientCertPath"`
TenantName string `gcfg:"tenant-name" mapstructure:"project-name" name:"os-projectName" value:"optional" dependsOn:"os-password|os-clientCertPath"`
UserDomainName string `gcfg:"user-domain-name" mapstructure:"user-domain-name" name:"os-userDomainName" value:"optional"`
TenantDomainName string `gcfg:"tenant-domain-name" mapstructure:"project-domain-name" name:"os-projectDomainName" value:"optional"`
Region string `name:"os-region"`
}
// String returns the string representation of the CloudINI.
func (i CloudINI) String() string {
// sanitize parameters to not include newlines
authURL := newlineRegexp.ReplaceAllString(i.AuthURL, "")
username := newlineRegexp.ReplaceAllString(i.Username, "")
password := newlineRegexp.ReplaceAllString(i.Password, "")
tenantID := newlineRegexp.ReplaceAllString(i.TenantID, "")
tenantName := newlineRegexp.ReplaceAllString(i.TenantName, "")
userDomainName := newlineRegexp.ReplaceAllString(i.UserDomainName, "")
tenantDomainName := newlineRegexp.ReplaceAllString(i.TenantDomainName, "")
region := newlineRegexp.ReplaceAllString(i.Region, "")
return fmt.Sprintf(`[Global]
auth-url = %s
username = %s
password = %s
tenant-id = %s
tenant-name = %s
user-domain-name = %s
tenant-domain-name = %s
region = %s
`, authURL, username, password, tenantID, tenantName, userDomainName, tenantDomainName, region)
}
var newlineRegexp = regexp.MustCompile(`[\r\n]+`)

View File

@ -0,0 +1,175 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package openstack
import (
"net/url"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAccountKeyFromURI(t *testing.T) {
accountKey := AccountKey{
AuthURL: "auth-url",
Username: "username",
Password: "password",
ProjectID: "project-id",
ProjectName: "project-name",
UserDomainName: "user-domain-name",
ProjectDomainName: "project-domain-name",
RegionName: "region-name",
}
testCases := map[string]struct {
cloudServiceAccountURI string
wantKey AccountKey
wantErr bool
}{
"successful": {
cloudServiceAccountURI: "serviceaccount://openstack?auth_url=auth-url&username=username&password=password&project_id=project-id&project_name=project-name&user_domain_name=user-domain-name&project_domain_name=project-domain-name&region_name=region-name",
wantKey: accountKey,
},
"missing auth_url": {
cloudServiceAccountURI: "serviceaccount://openstack?username=username&password=password&project_id=project-id&project_name=project-name&user_domain_name=user-domain-name&project_domain_name=project-domain-name&region_name=region-name",
wantErr: true,
},
"missing username": {
cloudServiceAccountURI: "serviceaccount://openstack?auth_url=auth-url&password=password&project_id=project-id&project_name=project-name&user_domain_name=user-domain-name&project_domain_name=project-domain-name&region_name=region-name",
wantErr: true,
},
"missing password": {
cloudServiceAccountURI: "serviceaccount://openstack?auth_url=auth-url&username=username&project_id=project-id&project_name=project-name&user_domain_name=user-domain-name&project_domain_name=project-domain-name&region_name=region-name",
wantErr: true,
},
"missing project_id": {
cloudServiceAccountURI: "serviceaccount://openstack?auth_url=auth-url&username=username&password=password&project_name=project-name&user_domain_name=user-domain-name&project_domain_name=project-domain-name&region_name=region-name",
wantErr: true,
},
"missing project_name": {
cloudServiceAccountURI: "serviceaccount://openstack?auth_url=auth-url&username=username&password=password&project_id=project-id&user_domain_name=user-domain-name&project_domain_name=project-domain-name&region_name=region-name",
wantErr: true,
},
"missing user_domain_name": {
cloudServiceAccountURI: "serviceaccount://openstack?auth_url=auth-url&username=username&password=password&project_id=project-id&project_name=project-name&project_domain_name=project-domain-name&region_name=region-name",
wantErr: true,
},
"missing project_domain_name": {
cloudServiceAccountURI: "serviceaccount://openstack?auth_url=auth-url&username=username&password=password&project_id=project-id&project_name=project-name&user_domain_name=user-domain-name&region_name=region-name",
wantErr: true,
},
"missing region_name": {
cloudServiceAccountURI: "serviceaccount://openstack?auth_url=auth-url&username=username&password=password&project_id=project-id&project_name=project-name&user_domain_name=user-domain-name&project_domain_name=project-domain-name",
wantErr: true,
},
"invalid URI fails": {
cloudServiceAccountURI: "\x00",
wantErr: true,
},
"incorrect URI scheme fails": {
cloudServiceAccountURI: "invalid",
wantErr: true,
},
"incorrect URI host fails": {
cloudServiceAccountURI: "serviceaccount://incorrect",
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
key, err := AccountKeyFromURI(tc.cloudServiceAccountURI)
if tc.wantErr {
assert.Error(err)
return
}
require.NoError(err)
assert.Equal(tc.wantKey, key)
})
}
}
func TestConvertToCloudServiceAccountURI(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
key := AccountKey{
AuthURL: "auth-url",
Username: "username",
Password: "password",
ProjectID: "project-id",
ProjectName: "project-name",
UserDomainName: "user-domain-name",
ProjectDomainName: "project-domain-name",
RegionName: "region-name",
}
accountURI := key.ToCloudServiceAccountURI()
uri, err := url.Parse(accountURI)
require.NoError(err)
query := uri.Query()
assert.Equal("serviceaccount", uri.Scheme)
assert.Equal("openstack", uri.Host)
assert.Equal(url.Values{
"auth_url": []string{"auth-url"},
"username": []string{"username"},
"password": []string{"password"},
"project_id": []string{"project-id"},
"project_name": []string{"project-name"},
"user_domain_name": []string{"user-domain-name"},
"project_domain_name": []string{"project-domain-name"},
"region_name": []string{"region-name"},
}, query)
}
func TestAccountKeyToCloudINI(t *testing.T) {
assert := assert.New(t)
key := AccountKey{
AuthURL: "auth-url",
Username: "username",
Password: "password",
ProjectID: "project-id",
ProjectName: "project-name",
UserDomainName: "user-domain-name",
ProjectDomainName: "project-domain-name",
RegionName: "region-name",
}
ini := key.CloudINI()
assert.Equal(CloudINI{
AuthURL: "auth-url",
Username: "username",
Password: "password",
TenantID: "project-id",
TenantName: "project-name",
UserDomainName: "user-domain-name",
TenantDomainName: "project-domain-name",
Region: "region-name",
}, ini)
}
func TestCloudINIToString(t *testing.T) {
ini := CloudINI{
AuthURL: "auth-url",
Username: "username",
Password: "password",
TenantID: "project-id",
TenantName: "project-name",
UserDomainName: "user-domain-name",
TenantDomainName: "project-domain-name",
Region: "region-name",
}
assert.Equal(t, `[Global]
auth-url = auth-url
username = username
password = password
tenant-id = project-id
tenant-name = project-name
user-domain-name = user-domain-name
tenant-domain-name = project-domain-name
region = region-name
`, ini.String())
}