diff --git a/api/types/authentication.go b/api/types/authentication.go index 6f61811aa6595..72c7437243e23 100644 --- a/api/types/authentication.go +++ b/api/types/authentication.go @@ -927,8 +927,27 @@ func (r *RequireMFAType) decode(val interface{}) error { } else { *r = RequireMFAType_OFF } + case int32: + return trace.Wrap(r.setFromEnum(v)) + case int64: + return trace.Wrap(r.setFromEnum(int32(v))) + case int: + return trace.Wrap(r.setFromEnum(int32(v))) + case float64: + return trace.Wrap(r.setFromEnum(int32(v))) + case float32: + return trace.Wrap(r.setFromEnum(int32(v))) default: return trace.BadParameter("RequireMFAType invalid type %T", val) } return nil } + +// setFromEnum sets the value from enum value as int32. +func (r *RequireMFAType) setFromEnum(val int32) error { + if _, ok := RequireMFAType_name[val]; !ok { + return trace.BadParameter("invalid required mfa mode %v", val) + } + *r = RequireMFAType(val) + return nil +} diff --git a/api/types/extension.go b/api/types/extension.go index e44cc4a95f02b..fcfb5ef78a74a 100644 --- a/api/types/extension.go +++ b/api/types/extension.go @@ -39,16 +39,40 @@ func (t CertExtensionType) MarshalJSON() ([]byte, error) { } func (t *CertExtensionType) UnmarshalJSON(b []byte) error { - var stringVal string - if err := json.Unmarshal(b, &stringVal); err != nil { + var anyVal any + if err := json.Unmarshal(b, &anyVal); err != nil { return err } - val, ok := certExtensionTypeValue[stringVal] - if !ok { - return trace.Errorf("invalid certificate extension type: %q", string(b)) + switch val := anyVal.(type) { + case string: + enumVal, ok := certExtensionTypeValue[val] + if !ok { + return trace.Errorf("invalid certificate extension type: %q", string(b)) + } + *t = enumVal + return nil + case int32: + return t.setFromEnum(val) + case int: + return t.setFromEnum(int32(val)) + case int64: + return t.setFromEnum(int32(val)) + case float64: + return trace.Wrap(t.setFromEnum(int32(val))) + case float32: + return trace.Wrap(t.setFromEnum(int32(val))) + default: + return trace.BadParameter("unexpected type %T", val) + } +} + +// setFromEnum sets the value from enum value as int32. +func (t *CertExtensionType) setFromEnum(val int32) error { + if _, ok := CertExtensionType_name[val]; !ok { + return trace.BadParameter("invalid cert extension mode %v", val) } - *t = val + *t = CertExtensionType(val) return nil } @@ -69,14 +93,38 @@ func (t CertExtensionMode) MarshalJSON() ([]byte, error) { } func (t *CertExtensionMode) UnmarshalJSON(b []byte) error { - var stringVal string - if err := json.Unmarshal(b, &stringVal); err != nil { + var anyVal any + if err := json.Unmarshal(b, &anyVal); err != nil { return err } - val, ok := certExtensionModeValue[stringVal] - if !ok { - return trace.Errorf("invalid certificate extension mode: %q", string(b)) + switch val := anyVal.(type) { + case string: + enumVal, ok := certExtensionModeValue[val] + if !ok { + return trace.Errorf("invalid certificate extension mode: %q", string(b)) + } + *t = enumVal + return nil + case int32: + return t.setFromEnum(val) + case int: + return t.setFromEnum(int32(val)) + case int64: + return t.setFromEnum(int32(val)) + case float64: + return trace.Wrap(t.setFromEnum(int32(val))) + case float32: + return trace.Wrap(t.setFromEnum(int32(val))) + default: + return trace.BadParameter("unexpected type %T", val) + } +} + +// setFromEnum sets the value from enum value as int32. +func (t *CertExtensionMode) setFromEnum(val int32) error { + if _, ok := CertExtensionMode_name[val]; !ok { + return trace.BadParameter("invalid cert extension mode %v", val) } - *t = val + *t = CertExtensionMode(val) return nil } diff --git a/api/types/role.go b/api/types/role.go index 16404de915e44..5dec33daf3d1f 100644 --- a/api/types/role.go +++ b/api/types/role.go @@ -1535,6 +1535,16 @@ func (h CreateHostUserMode) encode() (string, error) { func (h *CreateHostUserMode) decode(val any) error { var valS string switch val := val.(type) { + case int32: + return trace.Wrap(h.setFromEnum(val)) + case int64: + return trace.Wrap(h.setFromEnum(int32(val))) + case int: + return trace.Wrap(h.setFromEnum(int32(val))) + case float64: + return trace.Wrap(h.setFromEnum(int32(val))) + case float32: + return trace.Wrap(h.setFromEnum(int32(val))) case string: valS = val case bool: @@ -1543,7 +1553,7 @@ func (h *CreateHostUserMode) decode(val any) error { } valS = createHostUserModeOffString default: - return trace.BadParameter("bad value type %T, expected string", val) + return trace.BadParameter("bad value type %T, expected string or int", val) } switch valS { @@ -1561,6 +1571,15 @@ func (h *CreateHostUserMode) decode(val any) error { return nil } +// setFromEnum sets the value from enum value as int32. +func (h *CreateHostUserMode) setFromEnum(val int32) error { + if _, ok := CreateHostUserMode_name[val]; !ok { + return trace.BadParameter("invalid host user mode %v", val) + } + *h = CreateHostUserMode(val) + return nil +} + // UnmarshalYAML supports parsing CreateHostUserMode from string. func (h *CreateHostUserMode) UnmarshalYAML(unmarshal func(interface{}) error) error { var val interface{} diff --git a/api/types/role_test.go b/api/types/role_test.go new file mode 100644 index 0000000000000..22d51b4f528ec --- /dev/null +++ b/api/types/role_test.go @@ -0,0 +1,95 @@ +/* +Copyright 2023 Gravitational, Inc. + +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 types + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v2" +) + +func TestMarshallCreateHostUserModeJSON(t *testing.T) { + for _, tc := range []struct { + input CreateHostUserMode + expected string + }{ + {input: CreateHostUserMode_HOST_USER_MODE_OFF, expected: "off"}, + {input: CreateHostUserMode_HOST_USER_MODE_UNSPECIFIED, expected: ""}, + {input: CreateHostUserMode_HOST_USER_MODE_DROP, expected: "drop"}, + {input: CreateHostUserMode_HOST_USER_MODE_KEEP, expected: "keep"}, + } { + got, err := json.Marshal(&tc.input) + require.NoError(t, err) + require.Equal(t, fmt.Sprintf("%q", tc.expected), string(got)) + } +} + +func TestMarshallCreateHostUserModeYAML(t *testing.T) { + for _, tc := range []struct { + input CreateHostUserMode + expected string + }{ + {input: CreateHostUserMode_HOST_USER_MODE_OFF, expected: "\"off\""}, + {input: CreateHostUserMode_HOST_USER_MODE_UNSPECIFIED, expected: "\"\""}, + {input: CreateHostUserMode_HOST_USER_MODE_DROP, expected: "drop"}, + {input: CreateHostUserMode_HOST_USER_MODE_KEEP, expected: "keep"}, + } { + got, err := yaml.Marshal(&tc.input) + require.NoError(t, err) + require.Equal(t, fmt.Sprintf("%s\n", tc.expected), string(got)) + } +} + +func TestUnmarshallCreateHostUserModeJSON(t *testing.T) { + for _, tc := range []struct { + expected CreateHostUserMode + input any + }{ + {expected: CreateHostUserMode_HOST_USER_MODE_OFF, input: "\"off\""}, + {expected: CreateHostUserMode_HOST_USER_MODE_UNSPECIFIED, input: "\"\""}, + {expected: CreateHostUserMode_HOST_USER_MODE_DROP, input: "\"drop\""}, + {expected: CreateHostUserMode_HOST_USER_MODE_KEEP, input: "\"keep\""}, + {expected: CreateHostUserMode_HOST_USER_MODE_KEEP, input: 3}, + {expected: CreateHostUserMode_HOST_USER_MODE_OFF, input: 1}, + } { + var got CreateHostUserMode + err := json.Unmarshal([]byte(fmt.Sprintf("%v", tc.input)), &got) + require.NoError(t, err) + require.Equal(t, tc.expected, got) + } +} + +func TestUnmarshallCreateHostUserModeYAML(t *testing.T) { + for _, tc := range []struct { + expected CreateHostUserMode + input string + }{ + {expected: CreateHostUserMode_HOST_USER_MODE_OFF, input: "\"off\""}, + {expected: CreateHostUserMode_HOST_USER_MODE_OFF, input: "off"}, + {expected: CreateHostUserMode_HOST_USER_MODE_UNSPECIFIED, input: "\"\""}, + {expected: CreateHostUserMode_HOST_USER_MODE_DROP, input: "drop"}, + {expected: CreateHostUserMode_HOST_USER_MODE_KEEP, input: "keep"}, + } { + var got CreateHostUserMode + err := yaml.Unmarshal([]byte(tc.input), &got) + require.NoError(t, err) + require.Equal(t, tc.expected, got) + } +} diff --git a/examples/chart/teleport-cluster/charts/teleport-operator/templates/resources.teleport.dev_roles.yaml b/examples/chart/teleport-cluster/charts/teleport-operator/templates/resources.teleport.dev_roles.yaml index 263880749ca1c..248d086d5ba22 100644 --- a/examples/chart/teleport-cluster/charts/teleport-operator/templates/resources.teleport.dev_roles.yaml +++ b/examples/chart/teleport-cluster/charts/teleport-operator/templates/resources.teleport.dev_roles.yaml @@ -859,8 +859,7 @@ spec: mode: description: Mode is the type of extension to be used -- currently critical-option is not supported - format: int32 - type: integer + x-kubernetes-int-or-string: true name: description: Name specifies the key to be used in the cert extension. @@ -868,8 +867,7 @@ spec: type: description: Type represents the certificate type being extended, only ssh is supported at this time. - format: int32 - type: integer + x-kubernetes-int-or-string: true value: description: Value specifies the value to be used in the cert extension. @@ -898,8 +896,7 @@ spec: create_host_user_mode: description: CreateHostUserMode allows users to be automatically created on a host when not set to off - format: int32 - type: integer + x-kubernetes-int-or-string: true desktop_clipboard: description: DesktopClipboard indicates whether clipboard sharing is allowed between the user's workstation and the remote desktop. @@ -1010,8 +1007,7 @@ spec: require_session_mfa: description: RequireMFAType is the type of MFA requirement enforced for this user. - format: int32 - type: integer + x-kubernetes-int-or-string: true ssh_file_copy: description: SSHFileCopy indicates whether remote file operations via SCP or SFTP are allowed over an SSH session. It defaults @@ -1948,8 +1944,7 @@ spec: mode: description: Mode is the type of extension to be used -- currently critical-option is not supported - format: int32 - type: integer + x-kubernetes-int-or-string: true name: description: Name specifies the key to be used in the cert extension. @@ -1957,8 +1952,7 @@ spec: type: description: Type represents the certificate type being extended, only ssh is supported at this time. - format: int32 - type: integer + x-kubernetes-int-or-string: true value: description: Value specifies the value to be used in the cert extension. @@ -1987,8 +1981,7 @@ spec: create_host_user_mode: description: CreateHostUserMode allows users to be automatically created on a host when not set to off - format: int32 - type: integer + x-kubernetes-int-or-string: true desktop_clipboard: description: DesktopClipboard indicates whether clipboard sharing is allowed between the user's workstation and the remote desktop. @@ -2099,8 +2092,7 @@ spec: require_session_mfa: description: RequireMFAType is the type of MFA requirement enforced for this user. - format: int32 - type: integer + x-kubernetes-int-or-string: true ssh_file_copy: description: SSHFileCopy indicates whether remote file operations via SCP or SFTP are allowed over an SSH session. It defaults diff --git a/operator/config/crd/bases/resources.teleport.dev_roles.yaml b/operator/config/crd/bases/resources.teleport.dev_roles.yaml index 218077e935910..6c55e0c148c08 100644 --- a/operator/config/crd/bases/resources.teleport.dev_roles.yaml +++ b/operator/config/crd/bases/resources.teleport.dev_roles.yaml @@ -859,8 +859,7 @@ spec: mode: description: Mode is the type of extension to be used -- currently critical-option is not supported - format: int32 - type: integer + x-kubernetes-int-or-string: true name: description: Name specifies the key to be used in the cert extension. @@ -868,8 +867,7 @@ spec: type: description: Type represents the certificate type being extended, only ssh is supported at this time. - format: int32 - type: integer + x-kubernetes-int-or-string: true value: description: Value specifies the value to be used in the cert extension. @@ -898,8 +896,7 @@ spec: create_host_user_mode: description: CreateHostUserMode allows users to be automatically created on a host when not set to off - format: int32 - type: integer + x-kubernetes-int-or-string: true desktop_clipboard: description: DesktopClipboard indicates whether clipboard sharing is allowed between the user's workstation and the remote desktop. @@ -1010,8 +1007,7 @@ spec: require_session_mfa: description: RequireMFAType is the type of MFA requirement enforced for this user. - format: int32 - type: integer + x-kubernetes-int-or-string: true ssh_file_copy: description: SSHFileCopy indicates whether remote file operations via SCP or SFTP are allowed over an SSH session. It defaults @@ -1948,8 +1944,7 @@ spec: mode: description: Mode is the type of extension to be used -- currently critical-option is not supported - format: int32 - type: integer + x-kubernetes-int-or-string: true name: description: Name specifies the key to be used in the cert extension. @@ -1957,8 +1952,7 @@ spec: type: description: Type represents the certificate type being extended, only ssh is supported at this time. - format: int32 - type: integer + x-kubernetes-int-or-string: true value: description: Value specifies the value to be used in the cert extension. @@ -1987,8 +1981,7 @@ spec: create_host_user_mode: description: CreateHostUserMode allows users to be automatically created on a host when not set to off - format: int32 - type: integer + x-kubernetes-int-or-string: true desktop_clipboard: description: DesktopClipboard indicates whether clipboard sharing is allowed between the user's workstation and the remote desktop. @@ -2099,8 +2092,7 @@ spec: require_session_mfa: description: RequireMFAType is the type of MFA requirement enforced for this user. - format: int32 - type: integer + x-kubernetes-int-or-string: true ssh_file_copy: description: SSHFileCopy indicates whether remote file operations via SCP or SFTP are allowed over an SSH session. It defaults diff --git a/operator/controllers/resources/role_controller_test.go b/operator/controllers/resources/role_controller_test.go index 1e458d09774f2..ea502b1e86f6a 100644 --- a/operator/controllers/resources/role_controller_test.go +++ b/operator/controllers/resources/role_controller_test.go @@ -82,6 +82,26 @@ func TestRoleCreationFromYAML(t *testing.T) { shouldFail bool expectedSpec *types.RoleSpecV6 }{ + { + name: "Valid login list with integer create_host_user_mode", + roleSpecYAML: ` +allow: + logins: + - ubuntu + - root +options: + create_host_user_mode: 2 +`, + shouldFail: false, + expectedSpec: &types.RoleSpecV6{ + Allow: types.RoleConditions{ + Logins: []string{"ubuntu", "root"}, + }, + Options: types.RoleOptions{ + CreateHostUserMode: types.CreateHostUserMode_HOST_USER_MODE_DROP, + }, + }, + }, { name: "Valid login list", roleSpecYAML: ` @@ -89,12 +109,17 @@ allow: logins: - ubuntu - root +options: + create_host_user_mode: keep `, shouldFail: false, expectedSpec: &types.RoleSpecV6{ Allow: types.RoleConditions{ Logins: []string{"ubuntu", "root"}, }, + Options: types.RoleOptions{ + CreateHostUserMode: types.CreateHostUserMode_HOST_USER_MODE_KEEP, + }, }, }, { diff --git a/operator/crdgen/schemagen.go b/operator/crdgen/schemagen.go index c878ac06fc725..0dcb85f44ad17 100644 --- a/operator/crdgen/schemagen.go +++ b/operator/crdgen/schemagen.go @@ -270,9 +270,11 @@ func (generator *SchemaGenerator) singularProp(field *Field, prop *apiextv1.JSON case field.IsTime(): prop.Type = "string" prop.Format = "date-time" - case field.IsInt32() || field.IsUint32() || field.desc.IsEnum(): + case field.IsInt32() || field.IsUint32(): prop.Type = "integer" prop.Format = "int32" + case field.desc.IsEnum(): + prop.XIntOrString = true case field.IsInt64() || field.IsUint64(): prop.Type = "integer" prop.Format = "int64" diff --git a/operator/crdgen/testdata/golden/resources.teleport.dev_roles.yaml b/operator/crdgen/testdata/golden/resources.teleport.dev_roles.yaml index 646154598d499..de437959ab343 100644 --- a/operator/crdgen/testdata/golden/resources.teleport.dev_roles.yaml +++ b/operator/crdgen/testdata/golden/resources.teleport.dev_roles.yaml @@ -833,8 +833,7 @@ spec: mode: description: Mode is the type of extension to be used -- currently critical-option is not supported - format: int32 - type: integer + x-kubernetes-int-or-string: true name: description: Name specifies the key to be used in the cert extension. @@ -842,8 +841,7 @@ spec: type: description: Type represents the certificate type being extended, only ssh is supported at this time. - format: int32 - type: integer + x-kubernetes-int-or-string: true value: description: Value specifies the value to be used in the cert extension. @@ -975,8 +973,7 @@ spec: require_session_mfa: description: RequireMFAType is the type of MFA requirement enforced for this user. - format: int32 - type: integer + x-kubernetes-int-or-string: true ssh_file_copy: description: SSHFileCopy indicates whether remote file operations via SCP or SFTP are allowed over an SSH session. It defaults @@ -1887,8 +1884,7 @@ spec: mode: description: Mode is the type of extension to be used -- currently critical-option is not supported - format: int32 - type: integer + x-kubernetes-int-or-string: true name: description: Name specifies the key to be used in the cert extension. @@ -1896,8 +1892,7 @@ spec: type: description: Type represents the certificate type being extended, only ssh is supported at this time. - format: int32 - type: integer + x-kubernetes-int-or-string: true value: description: Value specifies the value to be used in the cert extension. @@ -2029,8 +2024,7 @@ spec: require_session_mfa: description: RequireMFAType is the type of MFA requirement enforced for this user. - format: int32 - type: integer + x-kubernetes-int-or-string: true ssh_file_copy: description: SSHFileCopy indicates whether remote file operations via SCP or SFTP are allowed over an SSH session. It defaults