1package okta
2
3import (
4	"context"
5	"fmt"
6	"net/url"
7
8	"time"
9
10	"github.com/chrismalek/oktasdk-go/okta"
11	cleanhttp "github.com/hashicorp/go-cleanhttp"
12	"github.com/hashicorp/vault/sdk/framework"
13	"github.com/hashicorp/vault/sdk/helper/tokenutil"
14	"github.com/hashicorp/vault/sdk/logical"
15)
16
17const (
18	defaultBaseURL = "okta.com"
19	previewBaseURL = "oktapreview.com"
20)
21
22func pathConfig(b *backend) *framework.Path {
23	p := &framework.Path{
24		Pattern: `config`,
25		Fields: map[string]*framework.FieldSchema{
26			"organization": &framework.FieldSchema{
27				Type:        framework.TypeString,
28				Description: "Use org_name instead.",
29				Deprecated:  true,
30			},
31			"org_name": &framework.FieldSchema{
32				Type:        framework.TypeString,
33				Description: "Name of the organization to be used in the Okta API.",
34				DisplayAttrs: &framework.DisplayAttributes{
35					Name: "Organization Name",
36				},
37			},
38			"token": &framework.FieldSchema{
39				Type:        framework.TypeString,
40				Description: "Use api_token instead.",
41				Deprecated:  true,
42			},
43			"api_token": &framework.FieldSchema{
44				Type:        framework.TypeString,
45				Description: "Okta API key.",
46				DisplayAttrs: &framework.DisplayAttributes{
47					Name: "API Token",
48				},
49			},
50			"base_url": &framework.FieldSchema{
51				Type:        framework.TypeString,
52				Description: `The base domain to use for the Okta API. When not specified in the configuration, "okta.com" is used.`,
53				DisplayAttrs: &framework.DisplayAttributes{
54					Name: "Base URL",
55				},
56			},
57			"production": &framework.FieldSchema{
58				Type:        framework.TypeBool,
59				Description: `Use base_url instead.`,
60				Deprecated:  true,
61			},
62			"ttl": &framework.FieldSchema{
63				Type:        framework.TypeDurationSecond,
64				Description: tokenutil.DeprecationText("token_ttl"),
65				Deprecated:  true,
66			},
67			"max_ttl": &framework.FieldSchema{
68				Type:        framework.TypeDurationSecond,
69				Description: tokenutil.DeprecationText("token_max_ttl"),
70				Deprecated:  true,
71			},
72			"bypass_okta_mfa": &framework.FieldSchema{
73				Type:        framework.TypeBool,
74				Description: `When set true, requests by Okta for a MFA check will be bypassed. This also disallows certain status checks on the account, such as whether the password is expired.`,
75				DisplayAttrs: &framework.DisplayAttributes{
76					Name: "Bypass Okta MFA",
77				},
78			},
79		},
80
81		Callbacks: map[logical.Operation]framework.OperationFunc{
82			logical.ReadOperation:   b.pathConfigRead,
83			logical.CreateOperation: b.pathConfigWrite,
84			logical.UpdateOperation: b.pathConfigWrite,
85		},
86
87		ExistenceCheck: b.pathConfigExistenceCheck,
88
89		HelpSynopsis: pathConfigHelp,
90	}
91
92	tokenutil.AddTokenFields(p.Fields)
93	p.Fields["token_policies"].Description += ". This will apply to all tokens generated by this auth method, in addition to any configured for specific users/groups."
94	return p
95}
96
97// Config returns the configuration for this backend.
98func (b *backend) Config(ctx context.Context, s logical.Storage) (*ConfigEntry, error) {
99	entry, err := s.Get(ctx, "config")
100	if err != nil {
101		return nil, err
102	}
103	if entry == nil {
104		return nil, nil
105	}
106
107	var result ConfigEntry
108	if entry != nil {
109		if err := entry.DecodeJSON(&result); err != nil {
110			return nil, err
111		}
112	}
113
114	if result.TokenTTL == 0 && result.TTL > 0 {
115		result.TokenTTL = result.TTL
116	}
117	if result.TokenMaxTTL == 0 && result.MaxTTL > 0 {
118		result.TokenMaxTTL = result.MaxTTL
119	}
120
121	return &result, nil
122}
123
124func (b *backend) pathConfigRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
125	cfg, err := b.Config(ctx, req.Storage)
126	if err != nil {
127		return nil, err
128	}
129	if cfg == nil {
130		return nil, nil
131	}
132
133	data := map[string]interface{}{
134		"organization":    cfg.Org,
135		"org_name":        cfg.Org,
136		"bypass_okta_mfa": cfg.BypassOktaMFA,
137	}
138	cfg.PopulateTokenData(data)
139
140	if cfg.BaseURL != "" {
141		data["base_url"] = cfg.BaseURL
142	}
143	if cfg.Production != nil {
144		data["production"] = *cfg.Production
145	}
146	if cfg.TTL > 0 {
147		data["ttl"] = int64(cfg.TTL.Seconds())
148	}
149	if cfg.MaxTTL > 0 {
150		data["max_ttl"] = int64(cfg.MaxTTL.Seconds())
151	}
152
153	resp := &logical.Response{
154		Data: data,
155	}
156
157	if cfg.BypassOktaMFA {
158		resp.AddWarning("Okta MFA bypass is configured. In addition to ignoring Okta MFA requests, certain other account statuses will not be seen, such as PASSWORD_EXPIRED. Authentication will succeed in these cases.")
159	}
160
161	return resp, nil
162}
163
164func (b *backend) pathConfigWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
165	cfg, err := b.Config(ctx, req.Storage)
166	if err != nil {
167		return nil, err
168	}
169
170	// Due to the existence check, entry will only be nil if it's a create
171	// operation, so just create a new one
172	if cfg == nil {
173		cfg = &ConfigEntry{}
174	}
175
176	org, ok := d.GetOk("org_name")
177	if ok {
178		cfg.Org = org.(string)
179	}
180	if cfg.Org == "" {
181		org, ok = d.GetOk("organization")
182		if ok {
183			cfg.Org = org.(string)
184		}
185	}
186	if cfg.Org == "" && req.Operation == logical.CreateOperation {
187		return logical.ErrorResponse("org_name is missing"), nil
188	}
189
190	token, ok := d.GetOk("api_token")
191	if ok {
192		cfg.Token = token.(string)
193	} else if token, ok = d.GetOk("token"); ok {
194		cfg.Token = token.(string)
195	}
196
197	baseURLRaw, ok := d.GetOk("base_url")
198	if ok {
199		baseURL := baseURLRaw.(string)
200		_, err = url.Parse(fmt.Sprintf("https://%s,%s", cfg.Org, baseURL))
201		if err != nil {
202			return logical.ErrorResponse(fmt.Sprintf("Error parsing given base_url: %s", err)), nil
203		}
204		cfg.BaseURL = baseURL
205	}
206
207	// We only care about the production flag when base_url is not set. It is
208	// for compatibility reasons.
209	if cfg.BaseURL == "" {
210		productionRaw, ok := d.GetOk("production")
211		if ok {
212			production := productionRaw.(bool)
213			cfg.Production = &production
214		}
215	} else {
216		// clear out old production flag if base_url is set
217		cfg.Production = nil
218	}
219
220	bypass, ok := d.GetOk("bypass_okta_mfa")
221	if ok {
222		cfg.BypassOktaMFA = bypass.(bool)
223	}
224
225	if err := cfg.ParseTokenFields(req, d); err != nil {
226		return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
227	}
228
229	// Handle upgrade cases
230	{
231		if err := tokenutil.UpgradeValue(d, "ttl", "token_ttl", &cfg.TTL, &cfg.TokenTTL); err != nil {
232			return logical.ErrorResponse(err.Error()), nil
233		}
234
235		if err := tokenutil.UpgradeValue(d, "max_ttl", "token_max_ttl", &cfg.MaxTTL, &cfg.TokenMaxTTL); err != nil {
236			return logical.ErrorResponse(err.Error()), nil
237		}
238	}
239
240	jsonCfg, err := logical.StorageEntryJSON("config", cfg)
241	if err != nil {
242		return nil, err
243	}
244	if err := req.Storage.Put(ctx, jsonCfg); err != nil {
245		return nil, err
246	}
247
248	var resp *logical.Response
249	if cfg.BypassOktaMFA {
250		resp = new(logical.Response)
251		resp.AddWarning("Okta MFA bypass is configured. In addition to ignoring Okta MFA requests, certain other account statuses will not be seen, such as PASSWORD_EXPIRED. Authentication will succeed in these cases.")
252	}
253
254	return resp, nil
255}
256
257func (b *backend) pathConfigExistenceCheck(ctx context.Context, req *logical.Request, d *framework.FieldData) (bool, error) {
258	cfg, err := b.Config(ctx, req.Storage)
259	if err != nil {
260		return false, err
261	}
262
263	return cfg != nil, nil
264}
265
266// OktaClient creates a basic okta client connection
267func (c *ConfigEntry) OktaClient() *okta.Client {
268	baseURL := defaultBaseURL
269	if c.Production != nil {
270		if !*c.Production {
271			baseURL = previewBaseURL
272		}
273	}
274	if c.BaseURL != "" {
275		baseURL = c.BaseURL
276	}
277
278	// We validate config on input and errors are only returned when parsing URLs
279	client, _ := okta.NewClientWithDomain(cleanhttp.DefaultClient(), c.Org, baseURL, c.Token)
280	return client
281}
282
283// ConfigEntry for Okta
284type ConfigEntry struct {
285	tokenutil.TokenParams
286
287	Org           string        `json:"organization"`
288	Token         string        `json:"token"`
289	BaseURL       string        `json:"base_url"`
290	Production    *bool         `json:"is_production,omitempty"`
291	TTL           time.Duration `json:"ttl"`
292	MaxTTL        time.Duration `json:"max_ttl"`
293	BypassOktaMFA bool          `json:"bypass_okta_mfa"`
294}
295
296const pathConfigHelp = `
297This endpoint allows you to configure the Okta and its
298configuration options.
299
300The Okta organization are the characters at the front of the URL for Okta.
301Example https://ORG.okta.com
302`
303