Unique Client ID base implementation (#122)

Unique Client ID base implementation
This commit is contained in:
Julien Stroheker 2020-02-17 14:41:15 -05:00 коммит произвёл GitHub
Родитель b2ae5b2eff
Коммит 3ed813c823
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
12 изменённых файлов: 219 добавлений и 10 удалений

Просмотреть файл

@ -89,6 +89,11 @@
"paths": [
"/clusterResourceGroupIdKey"
]
},
{
"paths": [
"/clientIdKey"
]
}
]
}

Просмотреть файл

@ -15,7 +15,8 @@
"type": "string"
},
"keyvaultPrefix": {
"type": "string"
"type": "string",
"maxLength": 20
},
"rpServicePrincipalId": {
"type": "string"
@ -38,7 +39,7 @@
},
"accessPolicies": []
},
"name": "[concat(parameters('keyvaultPrefix'), '-clusters')]",
"name": "[concat(parameters('keyvaultPrefix'), '-cls')]",
"type": "Microsoft.KeyVault/vaults",
"location": "[resourceGroup().location]",
"tags": {
@ -81,7 +82,7 @@
}
]
},
"name": "[concat(parameters('keyvaultPrefix'), '-service')]",
"name": "[concat(parameters('keyvaultPrefix'), '-svc')]",
"type": "Microsoft.KeyVault/vaults",
"location": "[resourceGroup().location]",
"tags": {

Просмотреть файл

@ -34,7 +34,8 @@
"type": "string"
},
"keyvaultPrefix": {
"type": "string"
"type": "string",
"maxLength": 20
},
"mdmCertificate": {
"type": "string"
@ -276,7 +277,7 @@
},
"accessPolicies": "[concat(variables('clustersKeyvaultAccessPolicies'), parameters('extraKeyvaultAccessPolicies'))]"
},
"name": "[concat(parameters('keyvaultPrefix'), '-clusters')]",
"name": "[concat(parameters('keyvaultPrefix'), '-cls')]",
"type": "Microsoft.KeyVault/vaults",
"location": "[resourceGroup().location]",
"tags": {
@ -293,7 +294,7 @@
},
"accessPolicies": "[concat(variables('serviceKeyvaultAccessPolicies'), parameters('extraKeyvaultAccessPolicies'))]"
},
"name": "[concat(parameters('keyvaultPrefix'), '-service')]",
"name": "[concat(parameters('keyvaultPrefix'), '-svc')]",
"type": "Microsoft.KeyVault/vaults",
"location": "[resourceGroup().location]",
"tags": {
@ -520,6 +521,11 @@
"paths": [
"/clusterResourceGroupIdKey"
]
},
{
"paths": [
"/clientIdKey"
]
}
]
}

Просмотреть файл

@ -43,7 +43,7 @@ var (
CloudErrorCodeInvalidParameter = "InvalidParameter"
CloudErrorCodeInvalidRequestContent = "InvalidRequestContent"
CloudErrorCodeInvalidResource = "InvalidResource"
CloudErrorCodeInvalidResourceGroup = "InvalidResourceGroup"
CloudErrorCodeDuplicateResourceGroup = "DuplicateResourceGroup"
CloudErrorCodeInvalidResourceNamespace = "InvalidResourceNamespace"
CloudErrorCodeInvalidResourceType = "InvalidResourceType"
CloudErrorCodeInvalidSubscriptionID = "InvalidSubscriptionID"
@ -64,6 +64,7 @@ var (
CloudErrorCodeInvalidServicePrincipalPermissions = "InvalidServicePrincipalPermissions"
CloudErrorCodeInvalidLocation = "InvalidLocation"
CloudErrorCodeInvalidOperationID = "InvalidOperationID"
CloudErrorCodeDuplicateClientID = "DuplicateClientID"
)
// NewCloudError returns a new CloudError

Просмотреть файл

@ -28,6 +28,7 @@ type OpenShiftClusterDocument struct {
Key string `json:"key,omitempty"`
PartitionKey string `json:"partitionKey,omitempty"`
ClusterResourceGroupIDKey string `json:"clusterResourceGroupIdKey,omitempty"`
ClientIDKey string `json:"clientIdKey,omitempty"`
Bucket int `json:"bucket,omitempty"`

Просмотреть файл

@ -36,6 +36,8 @@ type OpenShiftClusters interface {
Dequeue(context.Context) (*api.OpenShiftClusterDocument, error)
Lease(context.Context, string) (*api.OpenShiftClusterDocument, error)
EndLease(context.Context, string, api.ProvisioningState, api.ProvisioningState) (*api.OpenShiftClusterDocument, error)
GetByClientID(ctx context.Context, partitionKey, clientID string) (*api.OpenShiftClusterDocuments, error)
GetByClusterResourceGroupID(ctx context.Context, partitionKey, resourceGroupID string) (*api.OpenShiftClusterDocuments, error)
}
// NewOpenShiftClusters returns a new OpenShiftClusters
@ -293,3 +295,35 @@ func (c *openShiftClusters) partitionKey(key string) (string, error) {
r, err := azure.ParseResourceID(key)
return r.SubscriptionID, err
}
func (c *openShiftClusters) GetByClientID(ctx context.Context, partitionKey, clientID string) (*api.OpenShiftClusterDocuments, error) {
docs, err := c.c.QueryAll(ctx, partitionKey, &cosmosdb.Query{
Query: "SELECT * FROM OpenShiftClusters doc WHERE doc.clientIdKey = @clientID",
Parameters: []cosmosdb.Parameter{
{
Name: "@clientID",
Value: clientID,
},
},
}, nil)
if err != nil {
return nil, err
}
return docs, nil
}
func (c *openShiftClusters) GetByClusterResourceGroupID(ctx context.Context, partitionKey, resourceGroupID string) (*api.OpenShiftClusterDocuments, error) {
docs, err := c.c.QueryAll(ctx, partitionKey, &cosmosdb.Query{
Query: "SELECT * FROM OpenShiftClusters doc WHERE doc.clusterResourceGroupIdKey = @resourceGroupID",
Parameters: []cosmosdb.Parameter{
{
Name: "@resourceGroupID",
Value: resourceGroupID,
},
},
}, nil)
if err != nil {
return nil, err
}
return docs, nil
}

Просмотреть файл

@ -8,4 +8,6 @@ const (
KeyVaultTagName = "vault"
ClustersKeyVaultTagValue = "clusters"
ServiceKeyVaultTagValue = "service"
kvClusterSuffix = "-cls"
kvServiceSuffix = "-svc"
)

Просмотреть файл

@ -543,7 +543,7 @@ func (g *generator) clustersKeyvault() *arm.Resource {
},
AccessPolicies: &[]mgmtkeyvault.AccessPolicyEntry{},
},
Name: to.StringPtr("[concat(parameters('keyvaultPrefix'), '-clusters')]"),
Name: to.StringPtr("[concat(parameters('keyvaultPrefix'), '" + kvClusterSuffix + "')]"),
Type: to.StringPtr("Microsoft.KeyVault/vaults"),
Location: to.StringPtr("[resourceGroup().location]"),
Tags: map[string]*string{
@ -579,7 +579,7 @@ func (g *generator) serviceKeyvault() *arm.Resource {
},
AccessPolicies: &[]mgmtkeyvault.AccessPolicyEntry{},
},
Name: to.StringPtr("[concat(parameters('keyvaultPrefix'), '-service')]"),
Name: to.StringPtr("[concat(parameters('keyvaultPrefix'), '" + kvServiceSuffix + "')]"),
Type: to.StringPtr("Microsoft.KeyVault/vaults"),
Location: to.StringPtr("[resourceGroup().location]"),
Tags: map[string]*string{
@ -755,6 +755,11 @@ func (g *generator) database(databaseName string, addDependsOn bool) []*arm.Reso
"/clusterResourceGroupIdKey",
},
},
{
Paths: &[]string{
"/clientIdKey",
},
},
},
},
},
@ -915,6 +920,17 @@ func (g *generator) template() *arm.Template {
typ = "securestring"
}
t.Parameters[param] = &arm.TemplateParameter{Type: typ}
if param == "keyvaultPrefix" {
t.Parameters[param] = &arm.TemplateParameter{
Type: typ,
MaxLength: 24 - func(a, b int) int {
if a > b {
return a
}
return b
}(len(kvClusterSuffix), len(kvServiceSuffix)),
}
}
}
if g.production {

Просмотреть файл

@ -130,6 +130,7 @@ func (f *frontend) _putOrPatchOpenShiftCluster(ctx context.Context, r *http.Requ
if isCreate {
doc.ClusterResourceGroupIDKey = strings.ToLower(doc.OpenShiftCluster.Properties.ClusterProfile.ResourceGroupID)
doc.ClientIDKey = strings.ToLower(doc.OpenShiftCluster.Properties.ServicePrincipalProfile.ClientID)
doc.OpenShiftCluster.Properties.ProvisioningState = api.ProvisioningStateCreating
doc.OpenShiftCluster.Properties.ClusterProfile.Version = "4.3.0"
@ -166,7 +167,7 @@ func (f *frontend) _putOrPatchOpenShiftCluster(ctx context.Context, r *http.Requ
if isCreate {
newdoc, err := f.db.OpenShiftClusters.Create(ctx, doc)
if cosmosdb.IsErrorStatusCode(err, http.StatusPreconditionFailed) {
return nil, api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidResourceGroup, "", "The provided resource group '%s' already contains a cluster.", doc.OpenShiftCluster.Properties.ClusterProfile.ResourceGroupID)
return nil, f.validateOpenShiftUniqueKey(ctx, doc)
}
doc = newdoc
} else {

Просмотреть файл

@ -524,6 +524,114 @@ func TestPutOrPatchOpenShiftCluster(t *testing.T) {
wantStatusCode: http.StatusBadRequest,
wantError: "400: RequestNotAllowed: : Request is not allowed on cluster whose deletion failed. Delete the cluster.",
},
{
name: "creating cluster failing when provided cluster resource group already contains a cluster",
resourceID: fmt.Sprintf("/subscriptions/%s/resourcegroups/resourceGroup/providers/Microsoft.RedHatOpenShift/openshiftClusters/resourceName", mockSubID),
request: func(oc *v20191231preview.OpenShiftCluster) {
oc.Properties.ServicePrincipalProfile.ClientID = mockSubID
oc.Properties.ClusterProfile.ResourceGroupID = fmt.Sprintf("/subscriptions/%s/resourcegroups/aro-vjb21wca", mockSubID)
},
mocks: func(tt *test, asyncOperations *mock_database.MockAsyncOperations, openShiftClusters *mock_database.MockOpenShiftClusters) {
openShiftClusters.EXPECT().
Get(gomock.Any(), strings.ToLower(tt.resourceID)).
Return(nil, &cosmosdb.Error{StatusCode: http.StatusNotFound})
expectAsyncOperationDocumentCreate(asyncOperations, strings.ToLower(tt.resourceID), api.ProvisioningStateCreating)
clusterdoc := &api.OpenShiftClusterDocument{
Key: strings.ToLower(tt.resourceID),
Bucket: 1,
OpenShiftCluster: &api.OpenShiftCluster{
ID: tt.resourceID,
Name: "resourceName",
Type: "Microsoft.RedHatOpenShift/openshiftClusters",
Properties: api.Properties{
ProvisioningState: api.ProvisioningStateCreating,
ClusterProfile: api.ClusterProfile{
Version: "4.3.0",
ResourceGroupID: fmt.Sprintf("/subscriptions/%s/resourcegroups/aro-vjb21wca", mockSubID),
},
ServicePrincipalProfile: api.ServicePrincipalProfile{
TenantID: "11111111-1111-1111-1111-111111111111",
},
},
},
}
openShiftClusters.EXPECT().
Create(gomock.Any(), gomock.Any()).
Return(clusterdoc, &cosmosdb.Error{StatusCode: http.StatusPreconditionFailed})
openShiftClusters.EXPECT().
GetByClientID(gomock.Any(), clusterdoc.PartitionKey, mockSubID).
Return(&api.OpenShiftClusterDocuments{
Count: 0,
OpenShiftClusterDocuments: []*api.OpenShiftClusterDocument{},
}, nil)
openShiftClusters.EXPECT().
GetByClusterResourceGroupID(gomock.Any(), clusterdoc.PartitionKey, fmt.Sprintf("/subscriptions/%s/resourcegroups/aro-vjb21wca", mockSubID)).
Return(&api.OpenShiftClusterDocuments{
Count: 1,
OpenShiftClusterDocuments: []*api.OpenShiftClusterDocument{
&api.OpenShiftClusterDocument{
ClusterResourceGroupIDKey: fmt.Sprintf("/subscriptions/%s/resourcegroups/aro-vjb21wca", mockSubID),
},
},
}, nil)
},
wantAsync: true,
wantStatusCode: http.StatusBadRequest,
wantError: fmt.Sprintf("400: DuplicateResourceGroup: : The provided resource group '/subscriptions/%s/resourcegroups/aro-vjb21wca' already contains a cluster.", mockSubID),
},
{
name: "creating cluster failing when provided client ID is not unique",
resourceID: fmt.Sprintf("/subscriptions/%s/resourcegroups/resourceGroup/providers/Microsoft.RedHatOpenShift/openshiftClusters/resourceName", mockSubID),
request: func(oc *v20191231preview.OpenShiftCluster) {
oc.Properties.ServicePrincipalProfile.ClientID = mockSubID
},
mocks: func(tt *test, asyncOperations *mock_database.MockAsyncOperations, openShiftClusters *mock_database.MockOpenShiftClusters) {
openShiftClusters.EXPECT().
Get(gomock.Any(), strings.ToLower(tt.resourceID)).
Return(nil, &cosmosdb.Error{StatusCode: http.StatusNotFound})
expectAsyncOperationDocumentCreate(asyncOperations, strings.ToLower(tt.resourceID), api.ProvisioningStateCreating)
clusterdoc := &api.OpenShiftClusterDocument{
Key: strings.ToLower(tt.resourceID),
Bucket: 1,
OpenShiftCluster: &api.OpenShiftCluster{
ID: tt.resourceID,
Name: "resourceName",
Type: "Microsoft.RedHatOpenShift/openshiftClusters",
Properties: api.Properties{
ProvisioningState: api.ProvisioningStateCreating,
ClusterProfile: api.ClusterProfile{
Version: "4.3.0",
},
ServicePrincipalProfile: api.ServicePrincipalProfile{
TenantID: "11111111-1111-1111-1111-111111111111",
},
},
},
}
openShiftClusters.EXPECT().
Create(gomock.Any(), gomock.Any()).
Return(clusterdoc, &cosmosdb.Error{StatusCode: http.StatusPreconditionFailed})
openShiftClusters.EXPECT().
GetByClientID(gomock.Any(), clusterdoc.PartitionKey, mockSubID).
Return(&api.OpenShiftClusterDocuments{
Count: 1,
OpenShiftClusterDocuments: []*api.OpenShiftClusterDocument{
&api.OpenShiftClusterDocument{
ClientIDKey: mockSubID,
},
},
}, nil)
},
wantAsync: true,
wantStatusCode: http.StatusBadRequest,
wantError: fmt.Sprintf("400: DuplicateClientID: : The provided client ID '%s' is already in use by a cluster.", mockSubID),
},
} {
t.Run(tt.name, func(t *testing.T) {
defer cli.CloseIdleConnections()

Просмотреть файл

@ -44,3 +44,22 @@ func (f *frontend) validateSubscriptionState(ctx context.Context, key string, al
return nil, api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidSubscriptionState, "", "Request is not allowed in subscription in state '%s'.", doc.Subscription.State)
}
// validateOpenShiftUniqueKey returns which unique key if causing a 412 error
func (f *frontend) validateOpenShiftUniqueKey(ctx context.Context, doc *api.OpenShiftClusterDocument) error {
docs, err := f.db.OpenShiftClusters.GetByClientID(ctx, doc.PartitionKey, doc.ClientIDKey)
if err != nil {
return err
}
if docs.Count != 0 {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeDuplicateClientID, "", "The provided client ID '%s' is already in use by a cluster.", doc.OpenShiftCluster.Properties.ServicePrincipalProfile.ClientID)
}
docs, err = f.db.OpenShiftClusters.GetByClusterResourceGroupID(ctx, doc.PartitionKey, doc.ClusterResourceGroupIDKey)
if err != nil {
return err
}
if docs.Count != 0 {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeDuplicateResourceGroup, "", "The provided resource group '%s' already contains a cluster.", doc.OpenShiftCluster.Properties.ClusterProfile.ResourceGroupID)
}
return api.NewCloudError(http.StatusInternalServerError, api.CloudErrorCodeInternalServerError, "", "Internal server error.")
}

Просмотреть файл

@ -298,6 +298,21 @@ func (mr *MockOpenShiftClustersMockRecorder) Update(arg0, arg1 interface{}) *gom
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockOpenShiftClusters)(nil).Update), arg0, arg1)
}
// ValidateUniqueKey mocks base method
func (m *MockOpenShiftClusters) ValidateUniqueKey(arg0 context.Context, arg1, arg2, arg3 string) (*api.OpenShiftClusterDocuments, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ValidateUniqueKey", arg0, arg1, arg2, arg3)
ret0, _ := ret[0].(*api.OpenShiftClusterDocuments)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ValidateUniqueKey indicates an expected call of ValidateUniqueKey
func (mr *MockOpenShiftClustersMockRecorder) ValidateUniqueKey(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateUniqueKey", reflect.TypeOf((*MockOpenShiftClusters)(nil).ValidateUniqueKey), arg0, arg1, arg2, arg3)
}
// MockSubscriptions is a mock of Subscriptions interface
type MockSubscriptions struct {
ctrl *gomock.Controller