Introduce the Plugin Static Credentials object. (#27121)

* Introduce the Plugin Static Credentials object.

The Plugin Static Credentials object has been introduced to support storing
static credentials for plugins. The backend supporting the plugin static
credentials has been introduced as well.

Related RFD: https://github.com/gravitational/teleport/pull/26599

* Use MatchLabels instead of a custom rolled one, use named arguments for plugin static credentials interface, add type to error message.

* Remove ok references from credentials objects.

* Use protojson instead of jsonpb.

* Rename header unmarshal function.

* Update api/proto/teleport/legacy/types/types.proto

Co-authored-by: Sakshyam Shah <sshah@goteleport.com>

* Update api/types/plugin_static_credentials.go

Co-authored-by: Sakshyam Shah <sshah@goteleport.com>

* Use OAuth instead of Oauth, rename functions.

* Correct initialism.

* Use protojson instead of jsonpb, add an explanatory comment in MessageWithHeader.

---------

Co-authored-by: Sakshyam Shah <sshah@goteleport.com>
This commit is contained in:
Michael Wilson
2023-05-31 15:11:36 +00:00
committed by GitHub
co-authored by Sakshyam Shah
parent babd0c314f
commit bb4212c95d
9 changed files with 3285 additions and 1374 deletions
@@ -5255,6 +5255,48 @@ message PluginListV1 {
repeated PluginV1 plugins = 1;
}
// PluginStaticCredentialsV1 is a representation of static credentials for plugins.
message PluginStaticCredentialsV1 {
option (gogoproto.goproto_stringer) = false;
option (gogoproto.stringer) = false;
// Header is the resource header for the plugin static credentials object.
ResourceHeader Header = 1 [
(gogoproto.nullable) = false,
(gogoproto.embed) = true
];
// Spec contains the actual credentials for the object.
PluginStaticCredentialsSpecV1 Spec = 2;
}
// PluginStaticCredentialsSpecV1 is the specification for the static credentials object.
message PluginStaticCredentialsSpecV1 {
oneof credentials {
string APIToken = 1;
PluginStaticCredentialsBasicAuth BasicAuth = 2;
PluginStaticCredentialsOAuthClientSecret OAuthClientSecret = 3;
}
}
// PluginStaticCredentialsBasicAuth represents username and password credentials for a plugin.
message PluginStaticCredentialsBasicAuth {
// Username is the username to use for basic auth.
string Username = 1 [(gogoproto.jsontag) = "username"];
// Password is the password to use for basic auth.
string Password = 2 [(gogoproto.jsontag) = "password"];
}
// PluginStaticCredentialsOAuthClientSecret represents an oauth client id and secret.
message PluginStaticCredentialsOAuthClientSecret {
// ClientId is the client ID to use for OAuth client secret.
string ClientId = 1 [(gogoproto.jsontag) = "client_id"];
// ClientSecret is the client secret to use.
string ClientSecret = 2 [(gogoproto.jsontag) = "client_secret"];
}
// SAMLIdPServiceProviderV1 is the representation of a SAML IdP service provider.
message SAMLIdPServiceProviderV1 {
option (gogoproto.goproto_stringer) = false;
@@ -5752,3 +5794,32 @@ message JamfInventoryEntry {
// Defaults to "NOOP".
string on_missing = 4 [(gogoproto.jsontag) = "on_missing,omitempty"];
}
// MessageWithHeader is a message with a resource header. This is used primarily
// for parsing of resource headers and isn't expected to be used directly by any
// resources.
//
// When using a oneof in a protobuf messages, the existing utils.FastMarshal
// utility does not work, so using something like protojson or jsonpb is required.
// However, these do not respect gogoproto's extensions. When using a ResourceHeader,
// protojson will not recognize that the ResourceHeader is intended to be embedded and
// the resulting JSON will have the header as a separate field. This means that using
// utils.FastUnmarshal will not work for extracting a ResourceHeader from the
// JSON, and we explicitly extract this header to do things like version checking in
// lib/services.
//
// This can be avoided by explicitly embedding the members of the ResourceHeader in
// a message. However, if we would like to avoid this, we can use this MessageWitHheader
// to extract the resource header and its elements, which can later be used for the
// aforementioned processing in lib/services.
message MessageWithHeader {
option (gogoproto.goproto_stringer) = false;
option (gogoproto.stringer) = false;
// Header is the resource header for a resource.
ResourceHeader Header = 1 [
(gogoproto.nullable) = false,
(gogoproto.jsontag) = "",
(gogoproto.embed) = true
];
}
+3
View File
@@ -326,6 +326,9 @@ const (
// KindPlugin represents a plugin instance
KindPlugin = "plugin"
// KindPluginStaticCredentials represents plugin static credentials.
KindPluginStaticCredentials = "plugin_static_credentials"
// KindSAMLIdPServiceProvider is a SAML service provider for the built in Teleport IdP.
KindSAMLIdPServiceProvider = "saml_idp_service_provider"
+139
View File
@@ -0,0 +1,139 @@
/*
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 "github.com/gravitational/trace"
// PluginStaticCredentials are static credentials for plugins.
type PluginStaticCredentials interface {
// ResourceWithLabels provides common resource methods.
ResourceWithLabels
// GetAPIToken will return the attached API token if possible or empty if it is not present.
GetAPIToken() (apiToken string)
// GetBasicAuth will return the attached username and password. If they are not present, both
// the username and password will be mpty.
GetBasicAuth() (username string, password string)
// GetOAuthClientSecret will return the attached client ID and client secret. IF they are not
// present, the client ID and client secret will be empty.
GetOAuthClientSecret() (clientID string, clientSecret string)
}
// NewPluginStaticCredentials creates a new PluginStaticCredentialsV1 resource.
func NewPluginStaticCredentials(metadata Metadata, spec PluginStaticCredentialsSpecV1) (PluginStaticCredentials, error) {
p := &PluginStaticCredentialsV1{
ResourceHeader: ResourceHeader{
Metadata: metadata,
},
Spec: &spec,
}
if err := p.CheckAndSetDefaults(); err != nil {
return nil, trace.Wrap(err)
}
return p, nil
}
// CheckAndSetDefaults checks validity of all parameters and sets defaults.
func (p *PluginStaticCredentialsV1) CheckAndSetDefaults() error {
p.setStaticFields()
if err := p.Metadata.CheckAndSetDefaults(); err != nil {
return trace.Wrap(err)
}
switch credentials := p.Spec.Credentials.(type) {
case *PluginStaticCredentialsSpecV1_APIToken:
if credentials.APIToken == "" {
return trace.BadParameter("api token object is missing")
}
case *PluginStaticCredentialsSpecV1_BasicAuth:
if credentials.BasicAuth == nil {
return trace.BadParameter("basic auth object is missing")
}
if credentials.BasicAuth.Username == "" {
return trace.BadParameter("username is empty")
}
if credentials.BasicAuth.Password == "" {
return trace.BadParameter("password is empty")
}
case *PluginStaticCredentialsSpecV1_OAuthClientSecret:
if credentials.OAuthClientSecret == nil {
return trace.BadParameter("oauth client secret object is missing")
}
if credentials.OAuthClientSecret.ClientId == "" {
return trace.BadParameter("client ID is empty")
}
if credentials.OAuthClientSecret.ClientSecret == "" {
return trace.BadParameter("client secret is empty")
}
default:
return trace.BadParameter("credentials are not set or have an unknown type %T", credentials)
}
return nil
}
// setStaticFields sets static fields for the object.
func (p *PluginStaticCredentialsV1) setStaticFields() {
p.Kind = KindPluginStaticCredentials
p.Version = V1
}
// GetAPIToken will return the attached API token if possible or empty if it is not present.
func (p *PluginStaticCredentialsV1) GetAPIToken() (apiToken string) {
credentials, ok := p.Spec.Credentials.(*PluginStaticCredentialsSpecV1_APIToken)
if !ok {
return ""
}
return credentials.APIToken
}
// GetBasicAuth will return the attached username and password. If they are not present, both
// the username and password will be mpty.
func (p *PluginStaticCredentialsV1) GetBasicAuth() (username string, password string) {
credentials, ok := p.Spec.Credentials.(*PluginStaticCredentialsSpecV1_BasicAuth)
if !ok {
return "", ""
}
return credentials.BasicAuth.Username, credentials.BasicAuth.Password
}
// GetOAuthClientSecret will return the attached client ID and client secret. IF they are not
// present, the client ID and client secret will be empty.
func (p *PluginStaticCredentialsV1) GetOAuthClientSecret() (clientID string, clientSecret string) {
credentials, ok := p.Spec.Credentials.(*PluginStaticCredentialsSpecV1_OAuthClientSecret)
if !ok {
return "", ""
}
return credentials.OAuthClientSecret.ClientId, credentials.OAuthClientSecret.ClientSecret
}
// MatchSearch is a dummy value as credentials are not searchable.
func (p *PluginStaticCredentialsV1) MatchSearch(_ []string) bool {
return false
}
+2650 -1374
View File
File diff suppressed because it is too large Load Diff
+54
View File
@@ -0,0 +1,54 @@
/*
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 services
import (
"sync"
"github.com/gravitational/trace"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/protoadapt"
"github.com/gravitational/teleport/api/types"
)
var (
initHeaderJsonpbUnmarshaler sync.Once
protojsonOptions *protojson.UnmarshalOptions
)
// getHeaderProtoJSONOptions will return a singleton protojson options for unmarshaling.
func getHeaderProtoJSONOptions() *protojson.UnmarshalOptions {
initHeaderJsonpbUnmarshaler.Do(func() {
protojsonOptions = &protojson.UnmarshalOptions{
DiscardUnknown: true,
}
})
return protojsonOptions
}
// unmarshalHeaderWithProtoJSON will unmarshal the resource header using raw protojson.
// This is primarily of use for grpc messages that contain oneof and use the ResourceHeader,
// as utils.FastUnmarshal does not work for messages that use oneof.
func unmarshalHeaderWithProtoJSON(data []byte) (types.ResourceHeader, error) {
var h types.MessageWithHeader
if err := getHeaderProtoJSONOptions().Unmarshal(data, protoadapt.MessageV2Of(&h)); err != nil {
return types.ResourceHeader{}, trace.BadParameter(err.Error())
}
return h.ResourceHeader, nil
}
@@ -0,0 +1,88 @@
/*
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 local
import (
"context"
"github.com/gravitational/trace"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/backend"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/services/local/generic"
)
const (
pluginStaticCredentialsPrefix = "plugin_static_credentials"
)
// PluginStaticCredentialsService manages plugin static credentials in the Backend.
type PluginStaticCredentialsService struct {
svc generic.Service[types.PluginStaticCredentials]
}
// NewPluginStaticCredentialsService creates a new PluginStaticCredentialsService.
func NewPluginStaticCredentialsService(backend backend.Backend) (*PluginStaticCredentialsService, error) {
svc, err := generic.NewService(&generic.ServiceConfig[types.PluginStaticCredentials]{
Backend: backend,
ResourceKind: types.KindPluginStaticCredentials,
BackendPrefix: pluginStaticCredentialsPrefix,
MarshalFunc: services.MarshalPluginStaticCredentials,
UnmarshalFunc: services.UnmarshalPluginStaticCredentials,
})
if err != nil {
return nil, trace.Wrap(err)
}
return &PluginStaticCredentialsService{
svc: *svc,
}, nil
}
// CreatePluginStaticCredentials will create a new plugin static credentials resource.
func (p *PluginStaticCredentialsService) CreatePluginStaticCredentials(ctx context.Context, pluginStaticCredentials types.PluginStaticCredentials) error {
return p.svc.CreateResource(ctx, pluginStaticCredentials)
}
// GetPluginStaticCredentials will get a plugin static credentials resource by name.
func (p *PluginStaticCredentialsService) GetPluginStaticCredentials(ctx context.Context, name string) (types.PluginStaticCredentials, error) {
return p.svc.GetResource(ctx, name)
}
// GetPluginStaticCredentialsByLabels will get a list of plugin static credentials resource by matching labels.
func (p *PluginStaticCredentialsService) GetPluginStaticCredentialsByLabels(ctx context.Context, labels map[string]string) ([]types.PluginStaticCredentials, error) {
creds, err := p.svc.GetResources(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
var foundCredentials []types.PluginStaticCredentials
for _, cred := range creds {
if types.MatchLabels(cred, labels) {
foundCredentials = append(foundCredentials, cred)
}
}
return foundCredentials, nil
}
// DeletePluginStaticCredentials will delete a plugin static credentials resource.
func (p *PluginStaticCredentialsService) DeletePluginStaticCredentials(ctx context.Context, name string) error {
return p.svc.DeleteResource(ctx, name)
}
var _ services.PluginStaticCredentials = (*PluginStaticCredentialsService)(nil)
@@ -0,0 +1,133 @@
/*
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 local
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
"github.com/stretchr/testify/require"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/backend/memory"
)
// TestPluginStaticCredentialsCRUD tests backend operations with plugin static credentials resources.
func TestPluginStaticCredentialsCRUD(t *testing.T) {
ctx := context.Background()
backend, err := memory.New(memory.Config{
Context: ctx,
Clock: clockwork.NewFakeClock(),
})
require.NoError(t, err)
service, err := NewPluginStaticCredentialsService(backend)
require.NoError(t, err)
// Create a couple plugin static credentials.
cred1, err := types.NewPluginStaticCredentials(
types.Metadata{
Name: "cred1",
Labels: map[string]string{
"label1": "value1",
"label2": "value2",
},
},
types.PluginStaticCredentialsSpecV1{
Credentials: &types.PluginStaticCredentialsSpecV1_APIToken{
APIToken: "some-token",
},
})
require.NoError(t, err)
cred2, err := types.NewPluginStaticCredentials(
types.Metadata{
Name: "cred2",
Labels: map[string]string{
"label2": "value2",
"label3": "value3",
},
},
types.PluginStaticCredentialsSpecV1{
Credentials: &types.PluginStaticCredentialsSpecV1_APIToken{
APIToken: "some-token",
},
})
require.NoError(t, err)
// Create both plugin static credentials.
err = service.CreatePluginStaticCredentials(ctx, cred1)
require.NoError(t, err)
err = service.CreatePluginStaticCredentials(ctx, cred2)
require.NoError(t, err)
// Fetch static credentials by name.
cred, err := service.GetPluginStaticCredentials(ctx, cred1.GetName())
require.NoError(t, err)
require.Empty(t, cmp.Diff(cred1, cred,
cmpopts.IgnoreFields(types.Metadata{}, "ID"),
))
// Try to fetch a static credential that doesn't exist.
_, err = service.GetPluginStaticCredentials(ctx, "doesnotexist")
require.True(t, trace.IsNotFound(err))
// Try to create the same static credential.
err = service.CreatePluginStaticCredentials(ctx, cred1)
require.True(t, trace.IsAlreadyExists(err))
// Fetch static credentials by label.
creds, err := service.GetPluginStaticCredentialsByLabels(ctx, map[string]string{
"label1": "value1",
"label2": "value2",
})
require.NoError(t, err)
require.Empty(t, cmp.Diff([]types.PluginStaticCredentials{cred1}, creds,
cmpopts.IgnoreFields(types.Metadata{}, "ID"),
))
creds, err = service.GetPluginStaticCredentialsByLabels(ctx, map[string]string{
"label2": "value2",
})
require.NoError(t, err)
require.Empty(t, cmp.Diff([]types.PluginStaticCredentials{cred1, cred2}, creds,
cmpopts.IgnoreFields(types.Metadata{}, "ID"),
))
creds, err = service.GetPluginStaticCredentialsByLabels(ctx, map[string]string{
"label2": "value2",
"label3": "value3",
})
require.NoError(t, err)
require.Empty(t, cmp.Diff([]types.PluginStaticCredentials{cred2}, creds,
cmpopts.IgnoreFields(types.Metadata{}, "ID"),
))
// Delete a static credential.
err = service.DeletePluginStaticCredentials(ctx, cred1.GetName())
require.NoError(t, err)
_, err = service.GetPluginStaticCredentials(ctx, cred1.GetName())
require.True(t, trace.IsNotFound(err))
// Try to delete a static credential that doesn't exist.
err = service.DeletePluginStaticCredentials(ctx, "doesnotexist")
require.True(t, trace.IsNotFound(err))
}
+101
View File
@@ -0,0 +1,101 @@
/*
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 services
import (
"context"
"github.com/gravitational/trace"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/protoadapt"
"github.com/gravitational/teleport/api/types"
)
// PluginStaticCredentials is the plugin static credentials service
type PluginStaticCredentials interface {
// CreatePluginStaticCredentials will create a new plugin static credentials resource.
CreatePluginStaticCredentials(ctx context.Context, pluginStaticCredentials types.PluginStaticCredentials) error
// GetPluginStaticCredentials will get a plugin static credentials resource by name.
GetPluginStaticCredentials(ctx context.Context, name string) (types.PluginStaticCredentials, error)
// GetPluginStaticCredentialsByLabels will get a list of plugin static credentials resource by matching labels.
GetPluginStaticCredentialsByLabels(ctx context.Context, labels map[string]string) ([]types.PluginStaticCredentials, error)
// DeletePluginStaticCredentials will delete a plugin static credentials resource.
DeletePluginStaticCredentials(ctx context.Context, name string) error
}
// MarshalPluginStaticCredentials marshals PluginStaticCredentials resource to JSON.
func MarshalPluginStaticCredentials(pluginStaticCredentials types.PluginStaticCredentials, opts ...MarshalOption) ([]byte, error) {
if err := pluginStaticCredentials.CheckAndSetDefaults(); err != nil {
return nil, trace.Wrap(err)
}
cfg, err := CollectOptions(opts)
if err != nil {
return nil, trace.Wrap(err)
}
switch pluginStaticCredentials := pluginStaticCredentials.(type) {
case *types.PluginStaticCredentialsV1:
if !cfg.PreserveResourceID {
copy := *pluginStaticCredentials
copy.SetResourceID(0)
pluginStaticCredentials = &copy
}
data, err := protojson.Marshal(protoadapt.MessageV2Of(pluginStaticCredentials))
if err != nil {
return nil, trace.Wrap(err)
}
return data, nil
default:
return nil, trace.BadParameter("unsupported plugin static credentials resource %T", pluginStaticCredentials)
}
}
// UnmarshalPluginStaticCredentials unmarshals the plugin static credentials resource from JSON.
func UnmarshalPluginStaticCredentials(data []byte, opts ...MarshalOption) (types.PluginStaticCredentials, error) {
if len(data) == 0 {
return nil, trace.BadParameter("missing plugin static credentials resource data")
}
cfg, err := CollectOptions(opts)
if err != nil {
return nil, trace.Wrap(err)
}
h, err := unmarshalHeaderWithProtoJSON(data)
if err != nil {
return nil, trace.BadParameter(err.Error())
}
switch h.Version {
case types.V1:
var pluginStaticCredentials types.PluginStaticCredentialsV1
if err := protojson.Unmarshal(data, protoadapt.MessageV2Of(&pluginStaticCredentials)); err != nil {
return nil, trace.BadParameter(err.Error())
}
if err := pluginStaticCredentials.CheckAndSetDefaults(); err != nil {
return nil, trace.Wrap(err)
}
if cfg.ID != 0 {
pluginStaticCredentials.SetResourceID(cfg.ID)
}
if !cfg.Expires.IsZero() {
pluginStaticCredentials.SetExpiry(cfg.Expires)
}
return &pluginStaticCredentials, nil
}
return nil, trace.BadParameter("unsupported plugin static credentials resource version %q", h.Version)
}
@@ -0,0 +1,46 @@
/*
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 services
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
"github.com/gravitational/teleport/api/types"
)
func TestMarshalPluginStaticCredentialsRoundTrip(t *testing.T) {
spec := types.PluginStaticCredentialsSpecV1{
Credentials: &types.PluginStaticCredentialsSpecV1_APIToken{
APIToken: "some-token",
},
}
creds, err := types.NewPluginStaticCredentials(types.Metadata{
Name: "test-creds",
}, spec)
require.NoError(t, err)
payload, err := MarshalPluginStaticCredentials(creds)
require.NoError(t, err)
unmarshaled, err := UnmarshalPluginStaticCredentials(payload)
require.NoError(t, err)
require.Empty(t, cmp.Diff(creds, unmarshaled))
}