Skip to content

Commit

Permalink
LH-69472: Require that the API token provided is for a user with ADMI…
Browse files Browse the repository at this point in the history
…N or SUPER_ADMIN privileges (#17)

* verify only ADMIN and SUPER_ADMIN can perform actions

* add tests
  • Loading branch information
talhazi authored Aug 23, 2023
1 parent 74431ed commit 16633ec
Show file tree
Hide file tree
Showing 9 changed files with 165 additions and 105 deletions.
21 changes: 21 additions & 0 deletions provider/examples/resources/ios/ios_example.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
terraform {
required_providers {
cdo = {
source = "hashicorp.com/CiscoDevnet/cdo"
}
}
}

provider "cdo" {
base_url = "<https://www.defenseorchestrator.com|https://www.defenseorchestrator.eu|https://apj.cdo.cisco.com>"
api_token = "<replace-with-api-token-generated-from-cdo>"
}

resource "cdo_ios_device" "my_ios" {
name = "<name-of-device>"
connector_name = "<name-of-sdc-connector>"
socket_address = "<host>:<port>"
username = "<username>"
password = "<password>"
ignore_certificate = true
}
1 change: 1 addition & 0 deletions provider/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ require (
github.com/aws/aws-sdk-go-v2/config v1.18.27
github.com/cloudflare/circl v1.3.3 // indirect
github.com/fatih/color v1.15.0 // indirect
github.com/golang-jwt/jwt/v5 v5.0.0
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions provider/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4u
github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4=
github.com/go-git/go-git/v5 v5.6.1 h1:q4ZRqQl4pR/ZJHc1L5CFjGA1a10u76aV1iC+nh+bHsk=
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
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=
Expand Down
4 changes: 2 additions & 2 deletions provider/internal/device/asa/data_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (

cdoClient "github.com/CiscoDevnet/terraform-provider-cdo/go-client"
"github.com/CiscoDevnet/terraform-provider-cdo/go-client/device"
"github.com/CiscoDevnet/terraform-provider-cdo/validators"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
Expand Down Expand Up @@ -74,7 +74,7 @@ func (d *AsaDataSource) Schema(ctx context.Context, req datasource.SchemaRequest
MarkdownDescription: "The type of the connector that is used to communicate with the device. CDO can communicate with your device using either a Cloud Connector (CDG) or a Secure Device Connector (SDC); see [the CDO documentation](https://docs.defenseorchestrator.com/c-connect-cisco-defense-orchestratortor-the-secure-device-connector.html) to learn mor (Valid values: [CDG, SDC]).",
Computed: true,
Validators: []validator.String{
validators.OneOf("CDG", "SDC"),
stringvalidator.OneOf("CDG", "SDC"),
},
},
"socket_address": schema.StringAttribute{
Expand Down
3 changes: 2 additions & 1 deletion provider/internal/device/asa/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"strings"

"github.com/CiscoDevnet/terraform-provider-cdo/validators"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"

Expand Down Expand Up @@ -80,7 +81,7 @@ func (r *AsaDeviceResource) Schema(ctx context.Context, req resource.SchemaReque
MarkdownDescription: "The type of the connector that will be used to communicate with the device. CDO can communicate with your device using either a Cloud Connector (CDG) or a Secure Device Connector (SDC); see [the CDO documentation](https://docs.defenseorchestrator.com/c-connect-cisco-defense-orchestratortor-the-secure-device-connector.html) to learn mor (Valid values: [CDG, SDC]).",
Required: true,
Validators: []validator.String{
validators.OneOf("CDG", "SDC"),
stringvalidator.OneOf("CDG", "SDC"),
},
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
Expand Down
6 changes: 5 additions & 1 deletion provider/internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

cdoClient "github.com/CiscoDevnet/terraform-provider-cdo/go-client"
"github.com/CiscoDevnet/terraform-provider-cdo/validators"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/provider"
Expand Down Expand Up @@ -52,12 +53,15 @@ func (p *CdoProvider) Schema(ctx context.Context, req provider.SchemaRequest, re
MarkdownDescription: "The API token used to authenticate with CDO. [See here](https://docs.defenseorchestrator.com/c_api-tokens.html#!t-generatean-api-token.html) to learn how to generate an API token.",
Optional: true,
Sensitive: true,
Validators: []validator.String{
validators.OneOfRoles("ROLE_SUPER_ADMIN", "ROLE_ADMIN"),
},
},
"base_url": schema.StringAttribute{
MarkdownDescription: "The base CDO URL. This is the URL you enter when logging into your CDO account.",
Required: true,
Validators: []validator.String{
validators.OneOf("https://www.defenseorchestrator.com", "https://www.defenseorchestrator.eu", "https://apj.cdo.cisco.com", "https://staging.dev.lockhart.io", "https://ci.dev.lockhart.io", "https://scale.dev.lockhart.io"),
stringvalidator.OneOf("https://www.defenseorchestrator.com", "https://www.defenseorchestrator.eu", "https://apj.cdo.cisco.com", "https://staging.dev.lockhart.io", "https://ci.dev.lockhart.io", "https://scale.dev.lockhart.io"),
},
},
},
Expand Down
108 changes: 108 additions & 0 deletions provider/validators/cdo_role.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package validators

import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"strings"

"github.com/golang-jwt/jwt/v5"
"github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
)

const (
ErrInvalidTokenFormat = "invalid JWT token format"
ErrDecodeFailed = "failed to decode token payload"
ErrNoRolesFound = "no roles found in the JWT token"
)

var _ validator.String = oneOfRolesValidator{}

// oneOfRolesValidator validates that the value matches one of expected roles.
type oneOfRolesValidator struct {
expectedRoles []types.String
}

func (v oneOfRolesValidator) Description(ctx context.Context) string {
return v.MarkdownDescription(ctx)
}

func (v oneOfRolesValidator) MarkdownDescription(_ context.Context) string {
return fmt.Sprintf("The user must be assigned one of the following CDO roles: %q", v.expectedRoles)
}

func (v oneOfRolesValidator) ValidateString(ctx context.Context, request validator.StringRequest, response *validator.StringResponse) {
if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() {
return
}

token := request.ConfigValue.String()

role, err := extractRoleFromToken(token)

if err != nil {
fmt.Println("Error:", err)
}

for _, expectedRole := range v.expectedRoles {
if role == expectedRole.ValueString() {
return
}
}

response.Diagnostics.Append(validatordiag.InvalidAttributeValueMatchDiagnostic(
request.Path,
v.Description(ctx),
role,
))
}

func extractRoleFromToken(tokenString string) (string, error) {
if tokenString == "" {
return "", fmt.Errorf("tokenString is nil or empty")
}

parts := strings.Split(tokenString, ".")
if len(parts) != 3 {
return "", fmt.Errorf(ErrInvalidTokenFormat)
}

payloadBytes, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return "", fmt.Errorf(ErrDecodeFailed)
}

var claims jwt.MapClaims
if err := json.Unmarshal(payloadBytes, &claims); err != nil {
return "", fmt.Errorf("failed to decode token payload: %v", err)
}

if rolesClaim, exists := claims["roles"]; exists { // check if "roles" claim exists
if roles, ok := rolesClaim.([]interface{}); ok { // convert to a slice of interfaces
if len(roles) > 0 {
if role, ok := roles[0].(string); ok {
return role, nil
}
}
}
}

return "", fmt.Errorf(ErrNoRolesFound)
}

// OneOfRoles checks that the JWT token roles String held in the attribute
// is one of the given `roles`.
func OneOfRoles(roles ...string) validator.String {
frameworkValues := make([]types.String, 0, len(roles))

for _, value := range roles {
frameworkValues = append(frameworkValues, types.StringValue(value))
}

return oneOfRolesValidator{
expectedRoles: frameworkValues,
}
}
Original file line number Diff line number Diff line change
@@ -1,72 +1,56 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package validators_test

import (
"context"
"testing"

"github.com/CiscoDevnet/terraform-provider-cdo/validators"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"

"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
)

func TestOneOfValidator(t *testing.T) {
func TestOneOfRolesValidator(t *testing.T) {
t.Parallel()

SuperAdminRoleToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6WyJST0xFX1NVUEVSX0FETUlOIl19.FBEPzCpXPiYX6esud26WMrJvU3reLDVFdLociGrilVw"
AdminRoleToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6WyJST0xFX0FETUlOIl19.Xlk9JQJV9butj8ERu7mZFvRczY66KvmnTklQVnHwoy0"
BlaRoleToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6WyJST0xFX0JMQSJdfQ.nx10m1Cb8ZkUbxWKyfyJzsD1Y8rdTtkRbGEC2A4yV2M"
NoRolesToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6W119.0Dd6yUeJ4UbCr8WyXOiK3BhqVVwJFk5c53ipJBWenmc"

type testCase struct {
in types.String
validator validator.String
expErrors int
}

testCases := map[string]testCase{
"simple-match": {
in: types.StringValue("foo"),
validator: stringvalidator.OneOf(
"foo",
"bar",
"baz",
"super-admin-role-match": {
in: types.StringValue(SuperAdminRoleToken),
validator: validators.OneOfRoles(
"ROLE_SUPER_ADMIN", "ROLE_ADMIN",
),
expErrors: 0,
},
"simple-mismatch-case-insensitive": {
in: types.StringValue("foo"),
validator: stringvalidator.OneOf(
"FOO",
"bar",
"baz",
"admin-role-match": {
in: types.StringValue(AdminRoleToken),
validator: validators.OneOfRoles(
"ROLE_SUPER_ADMIN", "ROLE_ADMIN",
),
expErrors: 1,
expErrors: 0,
},
"simple-mismatch": {
in: types.StringValue("foz"),
validator: stringvalidator.OneOf(
"foo",
"bar",
"baz",
in: types.StringValue(BlaRoleToken),
validator: validators.OneOfRoles(
"ROLE_SUPER_ADMIN", "ROLE_ADMIN",
),
expErrors: 1,
},
"skip-validation-on-null": {
in: types.StringNull(),
validator: stringvalidator.OneOf(
"foo",
"bar",
"baz",
"no-roles": {
in: types.StringValue(NoRolesToken),
validator: validators.OneOfRoles(
"ROLE_SUPER_ADMIN", "ROLE_ADMIN",
),
expErrors: 0,
},
"skip-validation-on-unknown": {
in: types.StringUnknown(),
validator: stringvalidator.OneOf(
"foo",
"bar",
"baz",
),
expErrors: 0,
expErrors: 1,
},
}

Expand Down
61 changes: 0 additions & 61 deletions provider/validators/one_of.go

This file was deleted.

0 comments on commit 16633ec

Please sign in to comment.