mirror of
https://github.com/gravitational/teleport.git
synced 2026-09-24 16:17:11 +08:00
netiq: add plugin install prompt (#51502)
* netiq: add plugin install prompt This PR adds the plugin prompt code to read the required parameters to create the netiq plugin. Part of https://github.com/gravitational/access-graph/issues/634 Signed-off-by: Tiago Silva <tiago.silva@goteleport.com> * add license * Update netiq.go Co-authored-by: Gavin Frazar <gavin.frazar@goteleport.com> * Update netiq.go Co-authored-by: Gavin Frazar <gavin.frazar@goteleport.com> * Update netiq.go Co-authored-by: Gavin Frazar <gavin.frazar@goteleport.com> * handle review comments * Apply suggestions from code review Co-authored-by: Gavin Frazar <gavin.frazar@goteleport.com> * Apply suggestions from code review Co-authored-by: Gavin Frazar <gavin.frazar@goteleport.com> * consolidate prompt methods --------- Signed-off-by: Tiago Silva <tiago.silva@goteleport.com> Co-authored-by: Gavin Frazar <gavin.frazar@goteleport.com>
This commit is contained in:
co-authored by
Gavin Frazar
parent
e42d2005fc
commit
b61e70ea19
@@ -21,6 +21,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
urlpkg "net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
@@ -108,3 +109,45 @@ func Password(ctx context.Context, out io.Writer, in SecureReader, question stri
|
||||
}
|
||||
return string(answer), nil // passwords not trimmed
|
||||
}
|
||||
|
||||
// URLOptions are options for the URL prompt.
|
||||
type URLOptions func(*urlOptions)
|
||||
|
||||
type urlOptions struct {
|
||||
urlValidator func(*urlpkg.URL) error
|
||||
}
|
||||
|
||||
// WithURLValidator sets a custom URL validator for the URL prompt.
|
||||
func WithURLValidator(validator func(*urlpkg.URL) error) URLOptions {
|
||||
return func(opts *urlOptions) {
|
||||
opts.urlValidator = validator
|
||||
}
|
||||
}
|
||||
|
||||
// URL prompts the user for a URL. The prompt is written to out and the answer.
|
||||
func URL(ctx context.Context, out io.Writer, in Reader, question string, opts ...URLOptions) (string, error) {
|
||||
url, err := Input(ctx, out, in, question)
|
||||
if err != nil {
|
||||
return "", trace.Wrap(err, "failed reading prompt response")
|
||||
}
|
||||
|
||||
url = strings.TrimSpace(url)
|
||||
|
||||
u, err := urlpkg.Parse(url)
|
||||
if err != nil {
|
||||
return "", trace.Wrap(err, "invalid URL")
|
||||
}
|
||||
|
||||
opt := &urlOptions{}
|
||||
for _, o := range opts {
|
||||
o(opt)
|
||||
}
|
||||
|
||||
if opt.urlValidator != nil {
|
||||
if err := opt.urlValidator(u); err != nil {
|
||||
return "", trace.Wrap(err, "failed to verify url")
|
||||
}
|
||||
}
|
||||
|
||||
return url, nil
|
||||
}
|
||||
|
||||
@@ -17,12 +17,17 @@ limitations under the License.
|
||||
package prompt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
urlpkg "net/url"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/term"
|
||||
@@ -271,3 +276,82 @@ func (t *fakeTerm) Restore(fd int, oldState *term.State) error {
|
||||
t.restoreCalled = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestURL(t *testing.T) {
|
||||
pr, pw := io.Pipe()
|
||||
t.Cleanup(func() { pr.Close() })
|
||||
t.Cleanup(func() { pw.Close() })
|
||||
httpSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("hello"))
|
||||
}))
|
||||
t.Cleanup(httpSrv.Close)
|
||||
|
||||
write := func(t *testing.T, s string) {
|
||||
t.Helper()
|
||||
_, err := pw.Write([]byte(s))
|
||||
assert.NoError(t, err, "Write failed")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
cr := NewContextReader(pr)
|
||||
|
||||
t.Run("simple read", func(t *testing.T) {
|
||||
go write(t, httpSrv.URL)
|
||||
out := &bytes.Buffer{}
|
||||
gotURL, err := URL(ctx, out, cr, "Enter URL")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, httpSrv.URL, gotURL)
|
||||
require.Equal(t, "Enter URL: ", out.String())
|
||||
})
|
||||
|
||||
t.Run("read with validator", func(t *testing.T) {
|
||||
go write(t, httpSrv.URL)
|
||||
out := &bytes.Buffer{}
|
||||
|
||||
gotURL, err := URL(ctx, out, cr, "Enter URL", WithURLValidator(func(u *urlpkg.URL) error {
|
||||
rspBody, err := doHTTPCall(ctx, u.String())
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
if rspBody != "hello" {
|
||||
return trace.BadParameter("unexpected response body: %q", rspBody)
|
||||
}
|
||||
return nil
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, httpSrv.URL, gotURL)
|
||||
require.Equal(t, "Enter URL: ", out.String())
|
||||
})
|
||||
|
||||
t.Run("read with failed validator", func(t *testing.T) {
|
||||
go write(t, httpSrv.URL)
|
||||
out := &bytes.Buffer{}
|
||||
|
||||
_, err := URL(ctx, out, cr, "Enter URL", WithURLValidator(func(u *urlpkg.URL) error {
|
||||
rspBody, err := doHTTPCall(ctx, u.String())
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
if rspBody != "notHello" {
|
||||
return trace.BadParameter("unexpected response body: %q", rspBody)
|
||||
}
|
||||
return nil
|
||||
}))
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func doHTTPCall(ctx context.Context, u string) (string, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
|
||||
if err != nil {
|
||||
return "", trace.Wrap(err)
|
||||
}
|
||||
rsp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return "", trace.Wrap(err)
|
||||
}
|
||||
defer rsp.Body.Close()
|
||||
defer io.Copy(io.Discard, rsp.Body)
|
||||
b, err := io.ReadAll(rsp.Body)
|
||||
return string(b), trace.Wrap(err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,520 @@
|
||||
/*
|
||||
* Teleport
|
||||
* Copyright (C) 2025 Gravitational, Inc.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/alecthomas/kingpin/v2"
|
||||
"github.com/fatih/color"
|
||||
"github.com/gravitational/trace"
|
||||
|
||||
"github.com/gravitational/teleport"
|
||||
"github.com/gravitational/teleport/api"
|
||||
pluginspb "github.com/gravitational/teleport/api/gen/proto/go/teleport/plugins/v1"
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/api/utils/prompt"
|
||||
"github.com/gravitational/teleport/lib/defaults"
|
||||
)
|
||||
|
||||
var (
|
||||
boldGreen = color.New(color.Bold, color.FgGreen).SprintFunc()
|
||||
netIQStep1Template = bold("Step 1: Configure IDM OSP address") + `
|
||||
|
||||
Please provide the IDM OSP address to configure the integration.
|
||||
For example, 'https://idm.example.com/osp' or 'https://osp.idm.example.com'.
|
||||
`
|
||||
netIQStep2Template = bold("Step 2: Configure IDM API address") + `
|
||||
Please provide the IDM API (IDMProv or IDM Identity Applications) address to configure the integration.
|
||||
For example, 'https://idm.example.com/IDMProv' or 'https://idmapps.idm.example.com'.
|
||||
`
|
||||
netIQStep3Template = bold("Step 3: Configure IDM OSP OAuth Client") + `
|
||||
|
||||
The easiest way to register a new OAuth client with OSP (IDM Authorization Server) is to edit the ` + bold("ism-configuration.properties") + ` file by hand.
|
||||
The file is located in the ` + bold("osp-path/tomcat/conf/") + ` directory.
|
||||
To use an OAuth client with OSP, you must configure the following properties:
|
||||
` + boldGreen(` com.example.<client-id>.clientID = <client-id>
|
||||
com.example.<client-id>.clientPass = <secret>
|
||||
`) + `
|
||||
|
||||
If you prefer to store the client secret in a secure way, you can run the following command:
|
||||
|
||||
` + boldGreen(`java -jar /opt/netiq/idm/apps/tomcat/lib/obscurity-*jar <secret>`) + `
|
||||
|
||||
Then, copy the output and paste it into the ` + boldGreen(`com.example.<client-id>.clientPass`) + ` property as shown below:
|
||||
` + boldGreen(` com.example.<client-id>.clientID = <client-id>
|
||||
com.example.<client-id>.clientPass._attr_obscurity = ENCRYPT
|
||||
com.example.<client-id>.clientPass = <encrypted-secret>
|
||||
`) + `
|
||||
|
||||
After configuring the OAuth client, please restart OSP to apply the changes and type 'continue' to proceed.
|
||||
|
||||
`
|
||||
netIQStep4Template = bold("Step 4: Input Client ID and Client Secret") + `
|
||||
|
||||
With the values used in Step 2, please copy and paste the following information:
|
||||
`
|
||||
netIQStep5Template = bold("Step 5: Input IDM User and Password") + `
|
||||
|
||||
Please provide the IDM user to configure the integration.
|
||||
This user must have permissions to access the IDM API and retrieve users, groups, roles
|
||||
and resources. Use the following format: ` + bold("cn=uaadmin,ou=sa,o=data") + `.
|
||||
`
|
||||
)
|
||||
|
||||
type netIQArgs struct {
|
||||
cmd *kingpin.CmdClause
|
||||
insecureSkipVerify bool
|
||||
}
|
||||
|
||||
func (p *PluginsCommand) initInstallNetIQ(parent *kingpin.CmdClause) {
|
||||
p.install.netIQ.cmd = parent.Command("netiq", "Install an Access Graph NetIQ integration.")
|
||||
cmd := p.install.netIQ.cmd
|
||||
cmd.Flag("insecure-skip-verify", "Skip verification of the NetIQ server's SSL certificate.").BoolVar(&p.install.netIQ.insecureSkipVerify)
|
||||
}
|
||||
|
||||
type netIQSettings struct {
|
||||
apiURL string
|
||||
ospURL string
|
||||
identityVaultUser string
|
||||
identityVaultPassword string
|
||||
oAuthClientID string
|
||||
oAuthClientSecret string
|
||||
insecureSkipVerify bool
|
||||
}
|
||||
|
||||
func (p *PluginsCommand) netIQSetupGuide(ctx context.Context) (netIQSettings, error) {
|
||||
settings := netIQSettings{}
|
||||
var err error
|
||||
|
||||
settings.ospURL, err = promptForURL(
|
||||
ctx,
|
||||
os.Stdout,
|
||||
netIQStep1Template,
|
||||
"Please enter the IDM OSP address",
|
||||
p.install.netIQ.insecureSkipVerify,
|
||||
func(ctx context.Context, s *url.URL, b bool) error {
|
||||
_, err := checkNetIQOSPAddress(ctx, s.String(), b)
|
||||
return trace.Wrap(err)
|
||||
})
|
||||
if err != nil {
|
||||
return netIQSettings{}, trace.Wrap(err)
|
||||
}
|
||||
|
||||
settings.apiURL, err = promptForURL(
|
||||
ctx,
|
||||
os.Stdout,
|
||||
netIQStep2Template,
|
||||
"Please enter the IDM API address",
|
||||
p.install.netIQ.insecureSkipVerify,
|
||||
func(ctx context.Context, u *url.URL, b bool) error {
|
||||
return checkUnauthenticatedNetIQAPIAddress(ctx, u.String(), b)
|
||||
})
|
||||
if err != nil {
|
||||
return netIQSettings{}, trace.Wrap(err)
|
||||
}
|
||||
|
||||
if err := promptForContinue(ctx, os.Stdout, netIQStep3Template); err != nil {
|
||||
return netIQSettings{}, err
|
||||
}
|
||||
|
||||
settings.oAuthClientID, err = promptForInput(ctx, os.Stdout, netIQStep4Template, "Enter the OAuth ClientID")
|
||||
if err != nil {
|
||||
return netIQSettings{}, trace.Wrap(err)
|
||||
}
|
||||
|
||||
settings.oAuthClientSecret, err = promptForPassword(ctx, os.Stdout, "Enter the OAuth Client Secret")
|
||||
if err != nil {
|
||||
return netIQSettings{}, trace.Wrap(err)
|
||||
}
|
||||
|
||||
settings.identityVaultUser, err = promptForInput(ctx, os.Stdout, netIQStep5Template, "Enter the IDM User")
|
||||
if err != nil {
|
||||
return netIQSettings{}, trace.Wrap(err)
|
||||
}
|
||||
|
||||
settings.identityVaultPassword, err = promptForPassword(ctx, os.Stdout, "Enter the IDM User Password")
|
||||
if err != nil {
|
||||
return netIQSettings{}, trace.Wrap(err)
|
||||
}
|
||||
|
||||
settings.insecureSkipVerify = p.install.netIQ.insecureSkipVerify
|
||||
|
||||
// Validate the NetIQ settings.
|
||||
if err := checkAuthenticatedNetIQAPIAddress(ctx, settings); err != nil {
|
||||
return netIQSettings{}, trace.Wrap(err)
|
||||
}
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
func (p *PluginsCommand) InstallNetIQ(ctx context.Context, args installPluginArgs) error {
|
||||
settings, err := p.netIQSetupGuide(ctx)
|
||||
if err != nil {
|
||||
if errors.Is(err, errCancel) {
|
||||
return nil
|
||||
}
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
plugin, err := createNetIQPlugin(&settings)
|
||||
if err != nil {
|
||||
return trace.Wrap(err, "failed to create NetIQ plugin")
|
||||
}
|
||||
|
||||
creds, err := getNetIQPluginCredentials(settings)
|
||||
if err != nil {
|
||||
return trace.Wrap(err, "failed to get NetIQ plugin credentials")
|
||||
}
|
||||
|
||||
createPluginRequest := &pluginspb.CreatePluginRequest{
|
||||
Plugin: plugin,
|
||||
StaticCredentialsList: creds,
|
||||
CredentialLabels: map[string]string{
|
||||
netIQOrgURLLabel: settings.apiURL,
|
||||
},
|
||||
}
|
||||
|
||||
if _, err = args.plugins.CreatePlugin(ctx, createPluginRequest); err != nil {
|
||||
return trace.Wrap(err, "failed to create NetIQ plugin")
|
||||
}
|
||||
|
||||
fmt.Println("NetIQ plugin has been successfully installed.")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func promptForURL(ctx context.Context, out io.Writer, template, promptT string, insecureSkipVerify bool, checkFunc func(context.Context, *url.URL, bool) error) (string, error) {
|
||||
fmt.Fprintf(out, "\n%s", template)
|
||||
|
||||
return prompt.URL(
|
||||
ctx,
|
||||
out,
|
||||
prompt.Stdin(),
|
||||
promptT,
|
||||
prompt.WithURLValidator(
|
||||
func(u *url.URL) error {
|
||||
if err := checkFunc(ctx, u, insecureSkipVerify); err != nil {
|
||||
fmt.Fprintf(out, "Invalid address: %v\n", err)
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func promptForContinue(ctx context.Context, out io.Writer, template string) error {
|
||||
fmt.Fprintf(out, "\n%s", template)
|
||||
op, err := prompt.PickOne(
|
||||
ctx,
|
||||
out,
|
||||
prompt.Stdin(),
|
||||
"Type 'continue' to proceed, 'exit' to quit",
|
||||
[]string{"continue", "exit"},
|
||||
)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
if op == "exit" {
|
||||
return errCancel
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func promptForPassword(ctx context.Context, out io.Writer, promptMsg string) (string, error) {
|
||||
return prompt.Password(
|
||||
ctx, out, prompt.Stdin(),
|
||||
promptMsg,
|
||||
)
|
||||
}
|
||||
|
||||
func promptForInput(ctx context.Context, out io.Writer, template, promptT string) (string, error) {
|
||||
if template != "" {
|
||||
fmt.Fprintf(out, "\n%s", template)
|
||||
}
|
||||
return prompt.Input(ctx, out, prompt.Stdin(), promptT)
|
||||
}
|
||||
|
||||
func checkNetIQOSPAddress(ctx context.Context, ospURL string, insecureSkipVerify bool) (string, error) {
|
||||
var tokenAddr string
|
||||
err := checkNetIQAddress(
|
||||
ctx,
|
||||
ospURL,
|
||||
"/a/idm/auth/oauth2/.well-known/openid-configuration",
|
||||
insecureSkipVerify,
|
||||
func(r *http.Response) error {
|
||||
type openIDConfig struct {
|
||||
Token string `json:"token_endpoint"`
|
||||
}
|
||||
out := openIDConfig{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&out); err != nil || out.Token == "" {
|
||||
return trace.BadParameter("invalid OSP address")
|
||||
}
|
||||
tokenAddr = out.Token
|
||||
return nil
|
||||
},
|
||||
)
|
||||
return tokenAddr, trace.Wrap(err)
|
||||
}
|
||||
|
||||
// checkUnauthenticatedNetIQAPIAddress checks if the NetIQ API address is reachable and serves the expected content.
|
||||
func checkUnauthenticatedNetIQAPIAddress(ctx context.Context, apiAddr string, insecureSkipVerify bool) error {
|
||||
return checkNetIQAddress(
|
||||
ctx,
|
||||
apiAddr,
|
||||
"rest/access/info/version",
|
||||
insecureSkipVerify,
|
||||
// API endpoints are protected so we are ensuring that the endpoint
|
||||
// is reachable and doesn't return 404.
|
||||
func(r *http.Response) error {
|
||||
if r.StatusCode != http.StatusUnauthorized {
|
||||
return trace.BadParameter("invalid API address")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func checkAuthenticatedNetIQAPIAddress(ctx context.Context, data netIQSettings) error {
|
||||
tokenUrl, err := checkNetIQOSPAddress(ctx, data.ospURL, data.insecureSkipVerify)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
q := url.Values{}
|
||||
q.Add("grant_type", "password")
|
||||
q.Add("username", data.identityVaultUser)
|
||||
q.Add("password", data.identityVaultPassword)
|
||||
|
||||
// validate API credentials
|
||||
rsp, err := doRequest(
|
||||
ctx,
|
||||
tokenUrl,
|
||||
http.MethodPost,
|
||||
data.insecureSkipVerify,
|
||||
withBody(strings.NewReader(q.Encode())),
|
||||
withBasicAuth(data.oAuthClientID, data.oAuthClientSecret),
|
||||
withHeader("Content-Type", "application/x-www-form-urlencoded"),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
defer rsp.Body.Close()
|
||||
defer io.Copy(io.Discard, rsp.Body)
|
||||
|
||||
// tokenResponse represents the response from the OSP(NetIQ authorization service) token endpoint
|
||||
// when requesting an access token.
|
||||
type tokenResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
}
|
||||
var token tokenResponse
|
||||
if err := json.NewDecoder(rsp.Body).Decode(&token); err != nil {
|
||||
return trace.Wrap(err, "failed to decode response")
|
||||
}
|
||||
|
||||
return checkNetIQAddress(
|
||||
ctx,
|
||||
data.apiURL,
|
||||
"rest/access/info/version",
|
||||
data.insecureSkipVerify,
|
||||
// API endpoints are protected so we are ensuring that the endpoint
|
||||
// is reachable and doesn't return 404.
|
||||
func(r *http.Response) error {
|
||||
switch r.StatusCode {
|
||||
case http.StatusUnauthorized:
|
||||
return trace.BadParameter("invalid API credentials")
|
||||
case http.StatusOK:
|
||||
return nil
|
||||
default:
|
||||
return trace.BadParameter("invalid API address")
|
||||
}
|
||||
},
|
||||
withHeader("Authorization", fmt.Sprintf("%s %s", token.TokenType, token.AccessToken)),
|
||||
)
|
||||
}
|
||||
|
||||
func checkNetIQAddress(ctx context.Context, addr, path string, insecureSkipVerify bool, validation func(*http.Response) error, opts ...doRequestOptions) error {
|
||||
u, err := url.Parse(addr)
|
||||
if err != nil {
|
||||
return trace.Wrap(err, "invalid address")
|
||||
}
|
||||
u = u.JoinPath(path)
|
||||
rsp, err := doRequest(ctx, u.String(), http.MethodGet, insecureSkipVerify, opts...)
|
||||
if err != nil {
|
||||
return trace.Wrap(err, "failed to check address")
|
||||
}
|
||||
defer rsp.Body.Close()
|
||||
defer io.Copy(io.Discard, rsp.Body)
|
||||
|
||||
if err := validation(rsp); err != nil {
|
||||
return trace.Wrap(err, "failed to validate address")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type doRequestOptions func(*http.Request) error
|
||||
|
||||
func withBasicAuth(user, password string) doRequestOptions {
|
||||
return func(req *http.Request) error {
|
||||
req.SetBasicAuth(user, password)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func withBody(body io.Reader) doRequestOptions {
|
||||
return func(req *http.Request) error {
|
||||
req.Body = io.NopCloser(body)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func withHeader(key, value string) doRequestOptions {
|
||||
return func(req *http.Request) error {
|
||||
req.Header.Set(key, value)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func doRequest(ctx context.Context, url, method string, insecureSkipVerify bool, opts ...doRequestOptions) (*http.Response, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, method, url, nil)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("User-Agent", fmt.Sprintf("%s/%s", teleport.ComponentTCTL, api.Version))
|
||||
|
||||
for _, opt := range opts {
|
||||
if err := opt(req); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
transport, err := defaults.Transport()
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
if transport.TLSClientConfig == nil {
|
||||
transport.TLSClientConfig = &tls.Config{}
|
||||
}
|
||||
transport.TLSClientConfig.InsecureSkipVerify = insecureSkipVerify
|
||||
client := &http.Client{
|
||||
Transport: transport,
|
||||
}
|
||||
return client.Do(req)
|
||||
}
|
||||
|
||||
func getNetIQPluginCredentials(req netIQSettings) ([]*types.PluginStaticCredentialsV1, error) {
|
||||
var out []*types.PluginStaticCredentialsV1
|
||||
if req.oAuthClientID != "" {
|
||||
out = append(out, buildOAuthCredentials(req.oAuthClientID, req.oAuthClientSecret))
|
||||
}
|
||||
if req.identityVaultUser != "" {
|
||||
out = append(out, buildBasicAuthCredentials(req.identityVaultUser, req.identityVaultPassword))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
const (
|
||||
// netIQOrgURLLabel is the label for which NetIQ instance object belongs to.
|
||||
netIQOrgURLLabel = "netiq/org"
|
||||
// credPurposeLabel is the label for the purpose of the credential.
|
||||
credPurposeLabel = "netiq/purpose"
|
||||
// credPurposeNetIQOauth is the purpose for the NetIQ OAuth client ID and secret.
|
||||
credPurposeNetIQOauth = "oauth-client-id-secret"
|
||||
// credPurposeNetIQAuth is the purpose for the NetIQ Identity Vault user and password.
|
||||
credPurposeNetIQAuth = "netiq-auth"
|
||||
)
|
||||
|
||||
func buildOAuthCredentials(clientID, clientSecret string) *types.PluginStaticCredentialsV1 {
|
||||
return &types.PluginStaticCredentialsV1{
|
||||
ResourceHeader: types.ResourceHeader{
|
||||
Metadata: types.Metadata{
|
||||
Name: types.PluginTypeNetIQ + "-oauth",
|
||||
Labels: map[string]string{
|
||||
credPurposeLabel: credPurposeNetIQOauth,
|
||||
},
|
||||
},
|
||||
},
|
||||
Spec: &types.PluginStaticCredentialsSpecV1{
|
||||
Credentials: &types.PluginStaticCredentialsSpecV1_OAuthClientSecret{
|
||||
OAuthClientSecret: &types.PluginStaticCredentialsOAuthClientSecret{
|
||||
ClientId: clientID,
|
||||
ClientSecret: clientSecret,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildBasicAuthCredentials(user, password string) *types.PluginStaticCredentialsV1 {
|
||||
return &types.PluginStaticCredentialsV1{
|
||||
ResourceHeader: types.ResourceHeader{
|
||||
Metadata: types.Metadata{
|
||||
Name: types.PluginTypeNetIQ + "-basic-auth",
|
||||
Labels: map[string]string{
|
||||
credPurposeLabel: credPurposeNetIQAuth,
|
||||
},
|
||||
},
|
||||
},
|
||||
Spec: &types.PluginStaticCredentialsSpecV1{
|
||||
Credentials: &types.PluginStaticCredentialsSpecV1_BasicAuth{
|
||||
BasicAuth: &types.PluginStaticCredentialsBasicAuth{
|
||||
Username: user,
|
||||
Password: password,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func createNetIQPlugin(params *netIQSettings) (*types.PluginV1, error) {
|
||||
return &types.PluginV1{
|
||||
SubKind: types.PluginSubkindAccessGraph,
|
||||
Metadata: types.Metadata{
|
||||
Labels: map[string]string{
|
||||
types.TeleportNamespace + "/hosted-plugin": "true",
|
||||
},
|
||||
Name: types.PluginTypeNetIQ,
|
||||
},
|
||||
Spec: types.PluginSpecV1{
|
||||
Settings: &types.PluginSpecV1_NetIq{
|
||||
NetIq: &types.PluginNetIQSettings{
|
||||
OauthIssuerEndpoint: params.ospURL,
|
||||
ApiEndpoint: params.apiURL,
|
||||
InsecureSkipVerify: params.insecureSkipVerify,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -56,6 +56,7 @@ type pluginInstallArgs struct {
|
||||
okta oktaArgs
|
||||
scim scimArgs
|
||||
entraID entraArgs
|
||||
netIQ netIQArgs
|
||||
}
|
||||
|
||||
type scimArgs struct {
|
||||
@@ -102,6 +103,7 @@ func (p *PluginsCommand) initInstall(parent *kingpin.CmdClause, config *servicec
|
||||
p.initInstallOkta(p.install.cmd)
|
||||
p.initInstallSCIM(p.install.cmd)
|
||||
p.initInstallEntra(p.install.cmd)
|
||||
p.initInstallNetIQ(p.install.cmd)
|
||||
}
|
||||
|
||||
func (p *PluginsCommand) initInstallSCIM(parent *kingpin.CmdClause) {
|
||||
@@ -326,6 +328,8 @@ func (p *PluginsCommand) TryRun(ctx context.Context, cmd string, clientFunc comm
|
||||
commandFunc = p.InstallSCIM
|
||||
case p.install.entraID.cmd.FullCommand():
|
||||
commandFunc = p.InstallEntra
|
||||
case p.install.netIQ.cmd.FullCommand():
|
||||
commandFunc = p.InstallNetIQ
|
||||
case p.delete.cmd.FullCommand():
|
||||
commandFunc = p.Delete
|
||||
default:
|
||||
|
||||
Reference in New Issue
Block a user