mirror of
https://github.com/gravitational/teleport
synced 2024-10-20 01:03:40 +00:00
Integrations service for CRUD operations (#23989)
* Integrations service for CRUD operations This adds the services that will handle the CRUD operations. It also includes a custom JSON serializer. * improve integration.unmarshaljson * add trace.wrap to service * add CheckAndSetDefaults and godocs * Integration resource: remove status
This commit is contained in:
parent
c7671c774b
commit
a71e822db0
|
@ -338,6 +338,9 @@ const (
|
|||
// KindHeadlessAuthentication is a headless authentication resource.
|
||||
KindHeadlessAuthentication = "headless_authentication"
|
||||
|
||||
// KindIntegration is a connection to a 3rd party system API.
|
||||
KindIntegration = "integration"
|
||||
|
||||
// V6 is the sixth version of resources.
|
||||
V6 = "v6"
|
||||
|
||||
|
|
232
api/types/integration.go
Normal file
232
api/types/integration.go
Normal file
|
@ -0,0 +1,232 @@
|
|||
/*
|
||||
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"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
|
||||
"github.com/gravitational/teleport/api/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
// IntegrationSubKindAWSOIDC is an integration with AWS that uses OpenID Connect as an Identity Provider.
|
||||
IntegrationSubKindAWSOIDC = "aws-oidc"
|
||||
)
|
||||
|
||||
// Integration specifies is a connection configuration between Teleport and a 3rd party system.
|
||||
type Integration interface {
|
||||
ResourceWithLabels
|
||||
|
||||
// GetAWSOIDCIntegrationSpec returns the `aws-oidc` spec fields.
|
||||
GetAWSOIDCIntegrationSpec() *AWSOIDCIntegrationSpecV1
|
||||
// SetAWSOIDCIntegrationSpec sets the `aws-oidc` spec fields.
|
||||
SetAWSOIDCIntegrationSpec(*AWSOIDCIntegrationSpecV1)
|
||||
}
|
||||
|
||||
var _ ResourceWithLabels = (*IntegrationV1)(nil)
|
||||
|
||||
// NewIntegrationAWSOIDC returns a new `aws-oidc` subkind Integration
|
||||
func NewIntegrationAWSOIDC(md Metadata, spec *AWSOIDCIntegrationSpecV1) (*IntegrationV1, error) {
|
||||
ig := &IntegrationV1{
|
||||
ResourceHeader: ResourceHeader{
|
||||
Metadata: md,
|
||||
Kind: KindIntegration,
|
||||
Version: V1,
|
||||
SubKind: IntegrationSubKindAWSOIDC,
|
||||
},
|
||||
Spec: IntegrationSpecV1{
|
||||
SubKindSpec: &IntegrationSpecV1_AWSOIDC{
|
||||
AWSOIDC: spec,
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := ig.CheckAndSetDefaults(); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
return ig, nil
|
||||
}
|
||||
|
||||
// String returns the integration string representation.
|
||||
func (ig *IntegrationV1) String() string {
|
||||
return fmt.Sprintf("IntegrationV1(Name=%v, SubKind=%s, Labels=%v)",
|
||||
ig.GetName(), ig.GetSubKind(), ig.GetAllLabels())
|
||||
}
|
||||
|
||||
// MatchSearch goes through select field values and tries to
|
||||
// match against the list of search values.
|
||||
func (ig *IntegrationV1) MatchSearch(values []string) bool {
|
||||
fieldVals := append(utils.MapToStrings(ig.GetAllLabels()), ig.GetName(), ig.GetSubKind())
|
||||
return MatchSearch(fieldVals, values, nil)
|
||||
}
|
||||
|
||||
// setStaticFields sets static resource header and metadata fields.
|
||||
func (ig *IntegrationV1) setStaticFields() {
|
||||
ig.Kind = KindIntegration
|
||||
ig.Version = V1
|
||||
}
|
||||
|
||||
// CheckAndSetDefaults checks and sets default values
|
||||
func (ig *IntegrationV1) CheckAndSetDefaults() error {
|
||||
ig.setStaticFields()
|
||||
if err := ig.ResourceHeader.CheckAndSetDefaults(); err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
return trace.Wrap(ig.Spec.CheckAndSetDefaults())
|
||||
}
|
||||
|
||||
// CheckAndSetDefaults validates and sets default values for a integration.
|
||||
func (s *IntegrationSpecV1) CheckAndSetDefaults() error {
|
||||
if s.SubKindSpec == nil {
|
||||
return trace.BadParameter("missing required subkind spec")
|
||||
}
|
||||
|
||||
switch integrationSubKind := s.SubKindSpec.(type) {
|
||||
case *IntegrationSpecV1_AWSOIDC:
|
||||
err := integrationSubKind.CheckAndSetDefaults()
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
default:
|
||||
return trace.BadParameter("unknown integration subkind: %T", integrationSubKind)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckAndSetDefaults validates an agent mesh integration.
|
||||
func (s *IntegrationSpecV1_AWSOIDC) CheckAndSetDefaults() error {
|
||||
if s == nil || s.AWSOIDC == nil {
|
||||
return trace.BadParameter("aws_oidc is required for %q subkind", IntegrationSubKindAWSOIDC)
|
||||
}
|
||||
|
||||
if s.AWSOIDC.RoleARN == "" {
|
||||
return trace.BadParameter("role_arn is required for %q subkind", IntegrationSubKindAWSOIDC)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAWSOIDCIntegrationSpec returns the specific spec fields for `aws-oidc` subkind integrations.
|
||||
func (ig *IntegrationV1) GetAWSOIDCIntegrationSpec() *AWSOIDCIntegrationSpecV1 {
|
||||
return ig.Spec.GetAWSOIDC()
|
||||
}
|
||||
|
||||
// SetAWSOIDCIntegrationSpec sets the specific fields for the `aws-oidc` subkind integration.
|
||||
func (ig *IntegrationV1) SetAWSOIDCIntegrationSpec(awsOIDCSpec *AWSOIDCIntegrationSpecV1) {
|
||||
ig.Spec.SubKindSpec = &IntegrationSpecV1_AWSOIDC{
|
||||
AWSOIDC: awsOIDCSpec,
|
||||
}
|
||||
}
|
||||
|
||||
// Integrations is a list of Integration resources.
|
||||
type Integrations []Integration
|
||||
|
||||
// AsResources returns these groups as resources with labels.
|
||||
func (igs Integrations) AsResources() []ResourceWithLabels {
|
||||
resources := make([]ResourceWithLabels, len(igs))
|
||||
for i, ig := range igs {
|
||||
resources[i] = ig
|
||||
}
|
||||
return resources
|
||||
}
|
||||
|
||||
// Len returns the slice length.
|
||||
func (igs Integrations) Len() int { return len(igs) }
|
||||
|
||||
// Less compares integrations by name.
|
||||
func (igs Integrations) Less(i, j int) bool { return igs[i].GetName() < igs[j].GetName() }
|
||||
|
||||
// Swap swaps two integrations.
|
||||
func (igs Integrations) Swap(i, j int) { igs[i], igs[j] = igs[j], igs[i] }
|
||||
|
||||
// UnmarshalJSON is a custom unmarshaller for JSON format.
|
||||
// It is required because the Spec.SubKindSpec proto field is a oneof.
|
||||
// This translates into two issues when generating golang code:
|
||||
// - the Spec.SubKindSpec field in Go is an interface
|
||||
// - there's no way to provide json tags for oneof fields, so instead of snake_case, we get CamelCase for the Spec.SubKindSpec field
|
||||
//
|
||||
// Spec.SubKindSpec is an interface because it can have one of multiple values,
|
||||
// even though there's only one type for now: aws_oidc.
|
||||
// When trying to unmarshal this field, we must provide a concrete type.
|
||||
// To do so, we unmarshal just the root fields (ResourceHeader: Name, Kind, SubKind, Version, Metadata)
|
||||
// and then use its SubKind to provide a concrete type for the Spec.SubKindSpec field.
|
||||
// Unmarshalling the remaining fields uses the standard json.Unmarshal over the Spec field.
|
||||
//
|
||||
// Spec.SubKindSpec is expecting the `SubKindSpec` json tag, however we are using snake_case everywhere.
|
||||
// So, we create a local type that has the expected json tag (`sub_kind_spec`) and use it to unmarshal and then copy
|
||||
// to the proper type.
|
||||
func (ig *IntegrationV1) UnmarshalJSON(data []byte) error {
|
||||
var integration IntegrationV1
|
||||
|
||||
d := struct {
|
||||
ResourceHeader `json:""`
|
||||
Spec struct {
|
||||
RawSubKindSpec json.RawMessage `json:"subkind_spec"`
|
||||
} `json:"spec"`
|
||||
}{}
|
||||
|
||||
err := json.Unmarshal(data, &d)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
integration.ResourceHeader = d.ResourceHeader
|
||||
|
||||
var subkindSpec isIntegrationSpecV1_SubKindSpec
|
||||
switch integration.SubKind {
|
||||
case IntegrationSubKindAWSOIDC:
|
||||
subkindSpec = &IntegrationSpecV1_AWSOIDC{}
|
||||
default:
|
||||
return trace.BadParameter("invalid subkind %q", integration.ResourceHeader.SubKind)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(d.Spec.RawSubKindSpec, subkindSpec); err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
integration.Spec.SubKindSpec = subkindSpec
|
||||
|
||||
if err := integration.CheckAndSetDefaults(); err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
*ig = integration
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON is a custom marshaller for JSON format.
|
||||
// gogoproto doesn't allow for oneof json tags [https://github.com/gogo/protobuf/issues/623]
|
||||
// So, this is required to correctly use snake_case for every field.
|
||||
// Please see [IntegrationV1.UnmarshalJSON] for more information.
|
||||
func (ig *IntegrationV1) MarshalJSON() ([]byte, error) {
|
||||
d := struct {
|
||||
ResourceHeader `json:""`
|
||||
Spec struct {
|
||||
SubKindSpec isIntegrationSpecV1_SubKindSpec `json:"subkind_spec"`
|
||||
} `json:"spec"`
|
||||
}{}
|
||||
|
||||
d.ResourceHeader = ig.ResourceHeader
|
||||
d.Spec.SubKindSpec = ig.Spec.SubKindSpec
|
||||
|
||||
out, err := json.Marshal(d)
|
||||
return out, trace.Wrap(err)
|
||||
}
|
135
api/types/integration_test.go
Normal file
135
api/types/integration_test.go
Normal file
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
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"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/gravitational/trace"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/gravitational/teleport/api/defaults"
|
||||
)
|
||||
|
||||
func TestIntegrationJSONMarshalCycle(t *testing.T) {
|
||||
ig, err := NewIntegrationAWSOIDC(
|
||||
Metadata{Name: "some-integration"},
|
||||
&AWSOIDCIntegrationSpecV1{
|
||||
RoleARN: "arn:aws:iam::123456789012:role/DevTeams",
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
bs, err := json.Marshal(ig)
|
||||
require.NoError(t, err)
|
||||
|
||||
var ig2 IntegrationV1
|
||||
err = json.Unmarshal(bs, &ig2)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, ig, &ig2)
|
||||
}
|
||||
|
||||
func TestIntegrationCheckAndSetDefaults(t *testing.T) {
|
||||
noErrorFunc := func(err error) bool {
|
||||
return err == nil
|
||||
}
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
integration func(string) (*IntegrationV1, error)
|
||||
expectedIntegration func(string) *IntegrationV1
|
||||
expectedErrorIs func(error) bool
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
integration: func(name string) (*IntegrationV1, error) {
|
||||
return NewIntegrationAWSOIDC(
|
||||
Metadata{
|
||||
Name: name,
|
||||
},
|
||||
&AWSOIDCIntegrationSpecV1{
|
||||
RoleARN: "some arn role",
|
||||
},
|
||||
)
|
||||
},
|
||||
expectedIntegration: func(name string) *IntegrationV1 {
|
||||
return &IntegrationV1{
|
||||
ResourceHeader: ResourceHeader{
|
||||
Kind: KindIntegration,
|
||||
SubKind: IntegrationSubKindAWSOIDC,
|
||||
Version: V1,
|
||||
Metadata: Metadata{
|
||||
Name: name,
|
||||
Namespace: defaults.Namespace,
|
||||
},
|
||||
},
|
||||
Spec: IntegrationSpecV1{
|
||||
SubKindSpec: &IntegrationSpecV1_AWSOIDC{
|
||||
AWSOIDC: &AWSOIDCIntegrationSpecV1{
|
||||
RoleARN: "some arn role",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
expectedErrorIs: noErrorFunc,
|
||||
},
|
||||
{
|
||||
name: "aws-oidc: error when subkind spec is not provided",
|
||||
integration: func(name string) (*IntegrationV1, error) {
|
||||
return NewIntegrationAWSOIDC(
|
||||
Metadata{
|
||||
Name: name,
|
||||
},
|
||||
nil,
|
||||
)
|
||||
},
|
||||
expectedErrorIs: func(err error) bool {
|
||||
return trace.IsBadParameter(err)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "aws-oidc: error when no role is provided",
|
||||
integration: func(name string) (*IntegrationV1, error) {
|
||||
return NewIntegrationAWSOIDC(
|
||||
Metadata{
|
||||
Name: name,
|
||||
},
|
||||
&AWSOIDCIntegrationSpecV1{},
|
||||
)
|
||||
},
|
||||
expectedErrorIs: func(err error) bool {
|
||||
return trace.IsBadParameter(err)
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
name := uuid.NewString()
|
||||
ig, err := tt.integration(name)
|
||||
require.True(t, tt.expectedErrorIs(err), "expected another error", err)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
require.Equal(t, tt.expectedIntegration(name), ig)
|
||||
require.Contains(t, ig.String(), name)
|
||||
})
|
||||
}
|
||||
}
|
201
lib/auth/integration/integrationv1/service.go
Normal file
201
lib/auth/integration/integrationv1/service.go
Normal file
|
@ -0,0 +1,201 @@
|
|||
/*
|
||||
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 integrationv1
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
"github.com/sirupsen/logrus"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
|
||||
integrationpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/integration/v1"
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/lib/authz"
|
||||
"github.com/gravitational/teleport/lib/services"
|
||||
)
|
||||
|
||||
// ServiceConfig holds configuration options for
|
||||
// the Integration gRPC service.
|
||||
type ServiceConfig struct {
|
||||
Authorizer authz.Authorizer
|
||||
Cache services.IntegrationsGetter
|
||||
Backend services.Integrations
|
||||
Logger *logrus.Entry
|
||||
}
|
||||
|
||||
// CheckAndSetDefaults checks the ServiceConfig fields and returns an error if
|
||||
// a required param is not provided.
|
||||
// Authorizer, Cache and Backend are required params
|
||||
func (s *ServiceConfig) CheckAndSetDefaults() error {
|
||||
if s.Cache == nil {
|
||||
return trace.BadParameter("cache is required")
|
||||
}
|
||||
|
||||
if s.Backend == nil {
|
||||
return trace.BadParameter("backend is required")
|
||||
}
|
||||
|
||||
if s.Authorizer == nil {
|
||||
return trace.BadParameter("authorizer is required")
|
||||
}
|
||||
|
||||
if s.Logger == nil {
|
||||
s.Logger = logrus.WithField(trace.Component, "integrations.service")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Service implements the teleport.integration.v1.IntegrationService RPC service.
|
||||
type Service struct {
|
||||
integrationpb.UnimplementedIntegrationServiceServer
|
||||
authorizer authz.Authorizer
|
||||
cache services.IntegrationsGetter
|
||||
backend services.Integrations
|
||||
logger *logrus.Entry
|
||||
}
|
||||
|
||||
// NewService returns a new Integrations gRPC service.
|
||||
func NewService(cfg *ServiceConfig) (*Service, error) {
|
||||
if err := cfg.CheckAndSetDefaults(); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
return &Service{
|
||||
logger: cfg.Logger,
|
||||
authorizer: cfg.Authorizer,
|
||||
cache: cfg.Cache,
|
||||
backend: cfg.Backend,
|
||||
}, nil
|
||||
}
|
||||
|
||||
var _ integrationpb.IntegrationServiceServer = (*Service)(nil)
|
||||
|
||||
// ListIntegrations returns a paginated list of all Integration resources.
|
||||
func (s *Service) ListIntegrations(ctx context.Context, req *integrationpb.ListIntegrationsRequest) (*integrationpb.ListIntegrationsResponse, error) {
|
||||
_, err := authz.AuthorizeWithVerbs(ctx, s.logger, s.authorizer, true, types.KindIntegration, types.VerbRead, types.VerbList)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
results, nextKey, err := s.cache.ListIntegrations(ctx, int(req.GetLimit()), req.GetNextKey())
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
igs := make([]*types.IntegrationV1, len(results))
|
||||
for i, r := range results {
|
||||
v1, ok := r.(*types.IntegrationV1)
|
||||
if !ok {
|
||||
return nil, trace.BadParameter("unexpected Integration type %T", r)
|
||||
}
|
||||
igs[i] = v1
|
||||
}
|
||||
|
||||
return &integrationpb.ListIntegrationsResponse{
|
||||
Integrations: igs,
|
||||
NextKey: nextKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetIntegration returns the specified Integration resource.
|
||||
func (s *Service) GetIntegration(ctx context.Context, req *integrationpb.GetIntegrationRequest) (*types.IntegrationV1, error) {
|
||||
_, err := authz.AuthorizeWithVerbs(ctx, s.logger, s.authorizer, true, types.KindIntegration, types.VerbRead)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
integration, err := s.cache.GetIntegration(ctx, req.GetName())
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
igV1, ok := integration.(*types.IntegrationV1)
|
||||
if !ok {
|
||||
return nil, trace.BadParameter("unexpected Integration type %T", integration)
|
||||
}
|
||||
|
||||
return igV1, nil
|
||||
}
|
||||
|
||||
// CreateIntegration creates a new Okta import rule resource.
|
||||
func (s *Service) CreateIntegration(ctx context.Context, req *integrationpb.CreateIntegrationRequest) (*types.IntegrationV1, error) {
|
||||
_, err := authz.AuthorizeWithVerbs(ctx, s.logger, s.authorizer, true, types.KindIntegration, types.VerbCreate)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
ig, err := s.backend.CreateIntegration(ctx, req.GetIntegration())
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
igV1, ok := ig.(*types.IntegrationV1)
|
||||
if !ok {
|
||||
return nil, trace.BadParameter("unexpected Integration type %T", ig)
|
||||
}
|
||||
|
||||
return igV1, nil
|
||||
}
|
||||
|
||||
// UpdateIntegration updates an existing Okta import rule resource.
|
||||
func (s *Service) UpdateIntegration(ctx context.Context, req *integrationpb.UpdateIntegrationRequest) (*types.IntegrationV1, error) {
|
||||
_, err := authz.AuthorizeWithVerbs(ctx, s.logger, s.authorizer, true, types.KindIntegration, types.VerbUpdate)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
ig, err := s.backend.UpdateIntegration(ctx, req.GetIntegration())
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
igV1, ok := ig.(*types.IntegrationV1)
|
||||
if !ok {
|
||||
return nil, trace.BadParameter("unexpected Integration type %T", ig)
|
||||
}
|
||||
|
||||
return igV1, nil
|
||||
}
|
||||
|
||||
// DeleteIntegration removes the specified Integration resource.
|
||||
func (s *Service) DeleteIntegration(ctx context.Context, req *integrationpb.DeleteIntegrationRequest) (*emptypb.Empty, error) {
|
||||
_, err := authz.AuthorizeWithVerbs(ctx, s.logger, s.authorizer, true, types.KindIntegration, types.VerbDelete)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
if err := s.backend.DeleteIntegration(ctx, req.GetName()); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
// DeleteAllIntegrations removes all Integration resources.
|
||||
func (s *Service) DeleteAllIntegrations(ctx context.Context, _ *integrationpb.DeleteAllIntegrationsRequest) (*emptypb.Empty, error) {
|
||||
_, err := authz.AuthorizeWithVerbs(ctx, s.logger, s.authorizer, true, types.KindIntegration, types.VerbDelete)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
if err := s.backend.DeleteAllIntegrations(ctx); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
384
lib/auth/integration/integrationv1/service_test.go
Normal file
384
lib/auth/integration/integrationv1/service_test.go
Normal file
|
@ -0,0 +1,384 @@
|
|||
/*
|
||||
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 integrationv1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/gravitational/trace"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
integrationpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/integration/v1"
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/lib/authz"
|
||||
"github.com/gravitational/teleport/lib/backend/memory"
|
||||
"github.com/gravitational/teleport/lib/services"
|
||||
"github.com/gravitational/teleport/lib/services/local"
|
||||
"github.com/gravitational/teleport/lib/tlsca"
|
||||
)
|
||||
|
||||
func TestIntegrationCRUD(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, localClient, resourceSvc := initSvc(t, types.KindIntegration)
|
||||
|
||||
noError := func(err error) bool {
|
||||
return err == nil
|
||||
}
|
||||
|
||||
sampleIntegrationFn := func(t *testing.T, name string) types.Integration {
|
||||
ig, err := types.NewIntegrationAWSOIDC(
|
||||
types.Metadata{Name: name},
|
||||
&types.AWSOIDCIntegrationSpecV1{
|
||||
RoleARN: "arn:aws:iam::123456789012:role/OpsTeam",
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
return ig
|
||||
}
|
||||
|
||||
tt := []struct {
|
||||
Name string
|
||||
Role types.RoleSpecV6
|
||||
Setup func(t *testing.T, igName string)
|
||||
Test func(ctx context.Context, resourceSvc *Service, igName string) error
|
||||
ErrAssertion func(error) bool
|
||||
}{
|
||||
// Read
|
||||
{
|
||||
Name: "allowed read access to integrations",
|
||||
Role: types.RoleSpecV6{
|
||||
Allow: types.RoleConditions{Rules: []types.Rule{{
|
||||
Resources: []string{types.KindIntegration},
|
||||
Verbs: []string{types.VerbRead},
|
||||
}}},
|
||||
},
|
||||
Setup: func(t *testing.T, igName string) {
|
||||
_, err := localClient.CreateIntegration(ctx, sampleIntegrationFn(t, igName))
|
||||
require.NoError(t, err)
|
||||
},
|
||||
Test: func(ctx context.Context, resourceSvc *Service, igName string) error {
|
||||
_, err := resourceSvc.GetIntegration(ctx, &integrationpb.GetIntegrationRequest{
|
||||
Name: igName,
|
||||
})
|
||||
return err
|
||||
},
|
||||
ErrAssertion: noError,
|
||||
},
|
||||
{
|
||||
Name: "no access to read integrations",
|
||||
Role: types.RoleSpecV6{},
|
||||
Test: func(ctx context.Context, resourceSvc *Service, igName string) error {
|
||||
_, err := resourceSvc.GetIntegration(ctx, &integrationpb.GetIntegrationRequest{
|
||||
Name: igName,
|
||||
})
|
||||
return err
|
||||
},
|
||||
ErrAssertion: trace.IsAccessDenied,
|
||||
},
|
||||
{
|
||||
Name: "denied access to read integrations",
|
||||
Role: types.RoleSpecV6{
|
||||
Deny: types.RoleConditions{Rules: []types.Rule{{
|
||||
Resources: []string{types.KindIntegration},
|
||||
Verbs: []string{types.VerbRead},
|
||||
}}},
|
||||
},
|
||||
Test: func(ctx context.Context, resourceSvc *Service, igName string) error {
|
||||
_, err := resourceSvc.GetIntegration(ctx, &integrationpb.GetIntegrationRequest{
|
||||
Name: igName,
|
||||
})
|
||||
return err
|
||||
},
|
||||
ErrAssertion: trace.IsAccessDenied,
|
||||
},
|
||||
|
||||
// List
|
||||
{
|
||||
Name: "allowed list access to integrations",
|
||||
Role: types.RoleSpecV6{
|
||||
Allow: types.RoleConditions{Rules: []types.Rule{{
|
||||
Resources: []string{types.KindIntegration},
|
||||
Verbs: []string{types.VerbList, types.VerbRead},
|
||||
}}},
|
||||
},
|
||||
Setup: func(t *testing.T, _ string) {
|
||||
for i := 0; i < 10; i++ {
|
||||
_, err := localClient.CreateIntegration(ctx, sampleIntegrationFn(t, uuid.NewString()))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
},
|
||||
Test: func(ctx context.Context, resourceSvc *Service, igName string) error {
|
||||
_, err := resourceSvc.ListIntegrations(ctx, &integrationpb.ListIntegrationsRequest{
|
||||
Limit: 0,
|
||||
NextKey: "",
|
||||
})
|
||||
return err
|
||||
},
|
||||
ErrAssertion: noError,
|
||||
},
|
||||
{
|
||||
Name: "no list access to integrations",
|
||||
Role: types.RoleSpecV6{
|
||||
Allow: types.RoleConditions{Rules: []types.Rule{{
|
||||
Resources: []string{types.KindIntegration},
|
||||
Verbs: []string{types.VerbCreate},
|
||||
}}},
|
||||
},
|
||||
Test: func(ctx context.Context, resourceSvc *Service, igName string) error {
|
||||
_, err := resourceSvc.ListIntegrations(ctx, &integrationpb.ListIntegrationsRequest{
|
||||
Limit: 0,
|
||||
NextKey: "",
|
||||
})
|
||||
return err
|
||||
},
|
||||
ErrAssertion: trace.IsAccessDenied,
|
||||
},
|
||||
|
||||
// Create
|
||||
{
|
||||
Name: "no access to create integrations",
|
||||
Role: types.RoleSpecV6{},
|
||||
Test: func(ctx context.Context, resourceSvc *Service, igName string) error {
|
||||
ig := sampleIntegrationFn(t, igName)
|
||||
_, err := resourceSvc.CreateIntegration(ctx, &integrationpb.CreateIntegrationRequest{Integration: ig.(*types.IntegrationV1)})
|
||||
return err
|
||||
},
|
||||
ErrAssertion: trace.IsAccessDenied,
|
||||
},
|
||||
{
|
||||
Name: "access to create integrations",
|
||||
Role: types.RoleSpecV6{
|
||||
Allow: types.RoleConditions{Rules: []types.Rule{{
|
||||
Resources: []string{types.KindIntegration},
|
||||
Verbs: []string{types.VerbCreate},
|
||||
}}},
|
||||
},
|
||||
Test: func(ctx context.Context, resourceSvc *Service, igName string) error {
|
||||
ig := sampleIntegrationFn(t, igName)
|
||||
_, err := resourceSvc.CreateIntegration(ctx, &integrationpb.CreateIntegrationRequest{Integration: ig.(*types.IntegrationV1)})
|
||||
return err
|
||||
},
|
||||
ErrAssertion: noError,
|
||||
},
|
||||
|
||||
// Update
|
||||
{
|
||||
Name: "no access to update integration",
|
||||
Role: types.RoleSpecV6{},
|
||||
Test: func(ctx context.Context, resourceSvc *Service, igName string) error {
|
||||
ig := sampleIntegrationFn(t, igName)
|
||||
_, err := resourceSvc.UpdateIntegration(ctx, &integrationpb.UpdateIntegrationRequest{Integration: ig.(*types.IntegrationV1)})
|
||||
return err
|
||||
},
|
||||
ErrAssertion: trace.IsAccessDenied,
|
||||
},
|
||||
{
|
||||
Name: "access to update integration",
|
||||
Role: types.RoleSpecV6{
|
||||
Allow: types.RoleConditions{Rules: []types.Rule{{
|
||||
Resources: []string{types.KindIntegration},
|
||||
Verbs: []string{types.VerbUpdate},
|
||||
}}},
|
||||
},
|
||||
Setup: func(t *testing.T, igName string) {
|
||||
_, err := localClient.CreateIntegration(ctx, sampleIntegrationFn(t, igName))
|
||||
require.NoError(t, err)
|
||||
},
|
||||
Test: func(ctx context.Context, resourceSvc *Service, igName string) error {
|
||||
ig := sampleIntegrationFn(t, igName)
|
||||
_, err := resourceSvc.UpdateIntegration(ctx, &integrationpb.UpdateIntegrationRequest{Integration: ig.(*types.IntegrationV1)})
|
||||
return err
|
||||
},
|
||||
ErrAssertion: noError,
|
||||
},
|
||||
|
||||
// Delete
|
||||
{
|
||||
Name: "no access to delete integration",
|
||||
Role: types.RoleSpecV6{},
|
||||
Test: func(ctx context.Context, resourceSvc *Service, igName string) error {
|
||||
_, err := resourceSvc.DeleteIntegration(ctx, &integrationpb.DeleteIntegrationRequest{Name: "x"})
|
||||
return err
|
||||
},
|
||||
ErrAssertion: trace.IsAccessDenied,
|
||||
},
|
||||
{
|
||||
Name: "access to delete integration",
|
||||
Role: types.RoleSpecV6{
|
||||
Allow: types.RoleConditions{Rules: []types.Rule{{
|
||||
Resources: []string{types.KindIntegration},
|
||||
Verbs: []string{types.VerbDelete},
|
||||
}}},
|
||||
},
|
||||
Setup: func(t *testing.T, igName string) {
|
||||
_, err := localClient.CreateIntegration(ctx, sampleIntegrationFn(t, igName))
|
||||
require.NoError(t, err)
|
||||
},
|
||||
Test: func(ctx context.Context, resourceSvc *Service, igName string) error {
|
||||
_, err := resourceSvc.DeleteIntegration(ctx, &integrationpb.DeleteIntegrationRequest{Name: igName})
|
||||
return err
|
||||
},
|
||||
ErrAssertion: noError,
|
||||
},
|
||||
|
||||
// Delete all
|
||||
{
|
||||
Name: "remove all integrations fails when no access",
|
||||
Role: types.RoleSpecV6{},
|
||||
Test: func(ctx context.Context, resourceSvc *Service, igName string) error {
|
||||
_, err := resourceSvc.DeleteAllIntegrations(ctx, &integrationpb.DeleteAllIntegrationsRequest{})
|
||||
return err
|
||||
},
|
||||
ErrAssertion: trace.IsAccessDenied,
|
||||
},
|
||||
{
|
||||
Name: "remove all integrations",
|
||||
Role: types.RoleSpecV6{
|
||||
Allow: types.RoleConditions{Rules: []types.Rule{{
|
||||
Resources: []string{types.KindIntegration},
|
||||
Verbs: []string{types.VerbDelete},
|
||||
}}},
|
||||
},
|
||||
Setup: func(t *testing.T, _ string) {
|
||||
for i := 0; i < 10; i++ {
|
||||
_, err := localClient.CreateIntegration(ctx, sampleIntegrationFn(t, uuid.NewString()))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
},
|
||||
Test: func(ctx context.Context, resourceSvc *Service, igName string) error {
|
||||
_, err := resourceSvc.DeleteAllIntegrations(ctx, &integrationpb.DeleteAllIntegrationsRequest{})
|
||||
return err
|
||||
},
|
||||
ErrAssertion: noError,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
tc := tc
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
localCtx := authorizerForDummyUser(t, ctx, tc.Role, localClient)
|
||||
|
||||
igName := uuid.NewString()
|
||||
if tc.Setup != nil {
|
||||
tc.Setup(t, igName)
|
||||
}
|
||||
|
||||
err := tc.Test(localCtx, resourceSvc, igName)
|
||||
require.True(t, tc.ErrAssertion(err), err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func authorizerForDummyUser(t *testing.T, ctx context.Context, roleSpec types.RoleSpecV6, localClient localClient) context.Context {
|
||||
// Create role
|
||||
roleName := "role-" + uuid.NewString()
|
||||
role, err := types.NewRole(roleName, roleSpec)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = localClient.CreateRole(ctx, role)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create user
|
||||
user, err := types.NewUser("user-" + uuid.NewString())
|
||||
require.NoError(t, err)
|
||||
user.AddRole(roleName)
|
||||
err = localClient.CreateUser(user)
|
||||
require.NoError(t, err)
|
||||
|
||||
return authz.ContextWithUser(ctx, authz.LocalUser{
|
||||
Username: user.GetName(),
|
||||
Identity: tlsca.Identity{
|
||||
Username: user.GetName(),
|
||||
Groups: []string{role.GetName()},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
type localClient interface {
|
||||
CreateUser(user types.User) error
|
||||
CreateRole(ctx context.Context, role types.Role) error
|
||||
CreateIntegration(ctx context.Context, ig types.Integration) (types.Integration, error)
|
||||
}
|
||||
|
||||
func initSvc(t *testing.T, kind string) (context.Context, localClient, *Service) {
|
||||
ctx := context.Background()
|
||||
backend, err := memory.New(memory.Config{})
|
||||
require.NoError(t, err)
|
||||
|
||||
clusterConfigSvc, err := local.NewClusterConfigurationService(backend)
|
||||
require.NoError(t, err)
|
||||
trustSvc := local.NewCAService(backend)
|
||||
roleSvc := local.NewAccessService(backend)
|
||||
userSvc := local.NewIdentityService(backend)
|
||||
|
||||
require.NoError(t, clusterConfigSvc.SetAuthPreference(ctx, types.DefaultAuthPreference()))
|
||||
require.NoError(t, clusterConfigSvc.SetClusterAuditConfig(ctx, types.DefaultClusterAuditConfig()))
|
||||
require.NoError(t, clusterConfigSvc.SetClusterNetworkingConfig(ctx, types.DefaultClusterNetworkingConfig()))
|
||||
require.NoError(t, clusterConfigSvc.SetSessionRecordingConfig(ctx, types.DefaultSessionRecordingConfig()))
|
||||
|
||||
accessPoint := struct {
|
||||
services.ClusterConfiguration
|
||||
services.Trust
|
||||
services.RoleGetter
|
||||
services.UserGetter
|
||||
}{
|
||||
ClusterConfiguration: clusterConfigSvc,
|
||||
Trust: trustSvc,
|
||||
RoleGetter: roleSvc,
|
||||
UserGetter: userSvc,
|
||||
}
|
||||
|
||||
accessService := local.NewAccessService(backend)
|
||||
eventService := local.NewEventsService(backend)
|
||||
lockWatcher, err := services.NewLockWatcher(ctx, services.LockWatcherConfig{
|
||||
ResourceWatcherConfig: services.ResourceWatcherConfig{
|
||||
Client: eventService,
|
||||
Component: "test",
|
||||
},
|
||||
LockGetter: accessService,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
authorizer, err := authz.NewAuthorizer(authz.AuthorizerOpts{
|
||||
ClusterName: "test-cluster",
|
||||
AccessPoint: accessPoint,
|
||||
LockWatcher: lockWatcher,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
localResourceService, err := local.NewIntegrationsService(backend)
|
||||
require.NoError(t, err)
|
||||
|
||||
resourceSvc, err := NewService(&ServiceConfig{
|
||||
Backend: localResourceService,
|
||||
Authorizer: authorizer,
|
||||
Cache: localResourceService,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return ctx, struct {
|
||||
*local.AccessService
|
||||
*local.IdentityService
|
||||
*local.IntegrationsService
|
||||
}{
|
||||
AccessService: roleSvc,
|
||||
IdentityService: userSvc,
|
||||
IntegrationsService: localResourceService,
|
||||
}, resourceSvc
|
||||
}
|
98
lib/services/integration.go
Normal file
98
lib/services/integration.go
Normal file
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
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"
|
||||
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/lib/utils"
|
||||
)
|
||||
|
||||
// Integrations defines an interface for managing Integrations.
|
||||
type Integrations interface {
|
||||
IntegrationsGetter
|
||||
// CreateIntegration creates a new integration resource.
|
||||
CreateIntegration(context.Context, types.Integration) (types.Integration, error)
|
||||
// UpdateIntegration updates an existing integration resource.
|
||||
UpdateIntegration(context.Context, types.Integration) (types.Integration, error)
|
||||
// DeleteIntegration removes the specified integration resource.
|
||||
DeleteIntegration(ctx context.Context, name string) error
|
||||
// DeleteAllIntegrations removes all integrations.
|
||||
DeleteAllIntegrations(context.Context) error
|
||||
}
|
||||
|
||||
// IntegrationsGetter defines methods for List/Read operations on Integration Resources.
|
||||
type IntegrationsGetter interface {
|
||||
// ListIntegrations returns a paginated list of all integration resources.
|
||||
ListIntegrations(ctx context.Context, pageSize int, nextToken string) ([]types.Integration, string, error)
|
||||
// GetIntegration returns the specified integration resources.
|
||||
GetIntegration(ctx context.Context, name string) (types.Integration, error)
|
||||
}
|
||||
|
||||
// MarshalIntegration marshals the Integration resource to JSON.
|
||||
func MarshalIntegration(ig types.Integration, opts ...MarshalOption) ([]byte, error) {
|
||||
if err := ig.CheckAndSetDefaults(); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
cfg, err := CollectOptions(opts)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
switch g := ig.(type) {
|
||||
case *types.IntegrationV1:
|
||||
if !cfg.PreserveResourceID {
|
||||
copy := *g
|
||||
copy.SetResourceID(0)
|
||||
g = ©
|
||||
}
|
||||
return utils.FastMarshal(g)
|
||||
default:
|
||||
return nil, trace.BadParameter("unsupported integration resource %T", g)
|
||||
}
|
||||
}
|
||||
|
||||
// UnmarshalIntegration unmarshals Integration resource from JSON.
|
||||
func UnmarshalIntegration(data []byte, opts ...MarshalOption) (types.Integration, error) {
|
||||
if len(data) == 0 {
|
||||
return nil, trace.BadParameter("missing resource data")
|
||||
}
|
||||
|
||||
var ig types.IntegrationV1
|
||||
|
||||
err := utils.FastUnmarshal(data, &ig)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
cfg, err := CollectOptions(opts)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
if cfg.ID != 0 {
|
||||
ig.SetResourceID(cfg.ID)
|
||||
}
|
||||
if !cfg.Expires.IsZero() {
|
||||
ig.SetExpiry(cfg.Expires)
|
||||
}
|
||||
return &ig, nil
|
||||
}
|
60
lib/services/integration_test.go
Normal file
60
lib/services/integration_test.go
Normal file
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
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/stretchr/testify/require"
|
||||
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
)
|
||||
|
||||
func TestIntegrationMarshalCycle(t *testing.T) {
|
||||
ig, err := types.NewIntegrationAWSOIDC(
|
||||
types.Metadata{Name: "some-integration"},
|
||||
&types.AWSOIDCIntegrationSpecV1{
|
||||
RoleARN: "arn:aws:iam::123456789012:role/DevTeams",
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
bs, err := MarshalIntegration(ig)
|
||||
require.NoError(t, err)
|
||||
|
||||
ig2, err := UnmarshalIntegration(bs)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, ig, ig2)
|
||||
}
|
||||
|
||||
func TestIntegrationUnmarshal(t *testing.T) {
|
||||
ig, err := types.NewIntegrationAWSOIDC(
|
||||
types.Metadata{Name: "some-integration"},
|
||||
&types.AWSOIDCIntegrationSpecV1{
|
||||
RoleARN: "arn:aws:iam::123456789012:role/DevTeams",
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
storedBlob := []byte(`{"kind":"integration","sub_kind":"aws-oidc","version":"v1","metadata":{"name":"some-integration"},"spec":{"subkind_spec":{"aws_oidc":{"role_arn":"arn:aws:iam::123456789012:role/DevTeams"}}}}`)
|
||||
|
||||
ig2, err := UnmarshalIntegration(storedBlob)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, ig)
|
||||
|
||||
require.Equal(t, ig, ig2)
|
||||
}
|
113
lib/services/local/integrations.go
Normal file
113
lib/services/local/integrations.go
Normal file
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
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/defaults"
|
||||
"github.com/gravitational/teleport/lib/services"
|
||||
"github.com/gravitational/teleport/lib/services/local/generic"
|
||||
)
|
||||
|
||||
const (
|
||||
integrationsPrefix = "integrations"
|
||||
)
|
||||
|
||||
// IntegrationsService manages Integrations in the Backend.
|
||||
type IntegrationsService struct {
|
||||
svc generic.Service[types.Integration]
|
||||
}
|
||||
|
||||
// NewIntegrationsService creates a new IntegrationsService.
|
||||
func NewIntegrationsService(backend backend.Backend) (*IntegrationsService, error) {
|
||||
svc, err := generic.NewService(&generic.ServiceConfig[types.Integration]{
|
||||
Backend: backend,
|
||||
PageLimit: defaults.MaxIterationLimit,
|
||||
ResourceKind: types.KindIntegration,
|
||||
BackendPrefix: integrationsPrefix,
|
||||
MarshalFunc: services.MarshalIntegration,
|
||||
UnmarshalFunc: services.UnmarshalIntegration,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
return &IntegrationsService{
|
||||
svc: *svc,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListIntegrationss returns a paginated list of Integration resources.
|
||||
func (s *IntegrationsService) ListIntegrations(ctx context.Context, pageSize int, pageToken string) ([]types.Integration, string, error) {
|
||||
igs, nextKey, err := s.svc.ListResources(ctx, pageSize, pageToken)
|
||||
if err != nil {
|
||||
return nil, "", trace.Wrap(err)
|
||||
}
|
||||
|
||||
return igs, nextKey, nil
|
||||
}
|
||||
|
||||
// GetIntegrations returns the specified Integration resource.
|
||||
func (s *IntegrationsService) GetIntegration(ctx context.Context, name string) (types.Integration, error) {
|
||||
ig, err := s.svc.GetResource(ctx, name)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
return ig, nil
|
||||
}
|
||||
|
||||
// CreateIntegrations creates a new Integration resource.
|
||||
func (s *IntegrationsService) CreateIntegration(ctx context.Context, ig types.Integration) (types.Integration, error) {
|
||||
if err := ig.CheckAndSetDefaults(); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
if err := s.svc.CreateResource(ctx, ig); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
return ig, nil
|
||||
}
|
||||
|
||||
// UpdateIntegrations updates an existing Integration resource.
|
||||
func (s *IntegrationsService) UpdateIntegration(ctx context.Context, ig types.Integration) (types.Integration, error) {
|
||||
if err := ig.CheckAndSetDefaults(); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
if err := s.svc.UpdateResource(ctx, ig); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
return ig, nil
|
||||
}
|
||||
|
||||
// DeleteIntegrations removes the specified Integration resource.
|
||||
func (s *IntegrationsService) DeleteIntegration(ctx context.Context, name string) error {
|
||||
return trace.Wrap(s.svc.DeleteResource(ctx, name))
|
||||
}
|
||||
|
||||
// DeleteAllIntegrationss removes all Integration resources.
|
||||
func (s *IntegrationsService) DeleteAllIntegrations(ctx context.Context) error {
|
||||
return trace.Wrap(s.svc.DeleteAllResources(ctx))
|
||||
}
|
Loading…
Reference in a new issue