// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package approle

import (
	"context"
	"strings"
	"testing"
	"time"

	"github.com/hashicorp/vault/sdk/logical"
)

func TestAppRole_BoundCIDRLogin(t *testing.T) {
	var resp *logical.Response
	var err error
	b, s := createBackendWithStorage(t)

	// Create a role with secret ID binding disabled and only bound cidr list
	// enabled
	resp = b.requestNoErr(t, &logical.Request{
		Path:      "role/testrole",
		Operation: logical.CreateOperation,
		Data: map[string]interface{}{
			"bind_secret_id":    false,
			"bound_cidr_list":   []string{"127.0.0.1/8"},
			"token_bound_cidrs": []string{"10.0.0.0/8"},
		},
		Storage: s,
	})

	// Read the role ID
	resp = b.requestNoErr(t, &logical.Request{
		Path:      "role/testrole/role-id",
		Operation: logical.ReadOperation,
		Storage:   s,
	})

	roleID := resp.Data["role_id"]

	// Fill in the connection information and login with just the role ID
	resp = b.requestNoErr(t, &logical.Request{
		Path:      "login",
		Operation: logical.UpdateOperation,
		Data: map[string]interface{}{
			"role_id": roleID,
		},
		Storage:    s,
		Connection: &logical.Connection{RemoteAddr: "127.0.0.1"},
	})

	if resp.Auth == nil {
		t.Fatal("expected login to succeed")
	}
	if len(resp.Auth.BoundCIDRs) != 1 {
		t.Fatal("bad token bound cidrs")
	}
	if resp.Auth.BoundCIDRs[0].String() != "10.0.0.0/8" {
		t.Fatalf("bad: %s", resp.Auth.BoundCIDRs[0].String())
	}

	// Override with a secret-id value, verify it doesn't pass
	resp = b.requestNoErr(t, &logical.Request{
		Path:      "role/testrole",
		Operation: logical.UpdateOperation,
		Data: map[string]interface{}{
			"bind_secret_id": true,
		},
		Storage: s,
	})

	roleSecretIDReq := &logical.Request{
		Operation: logical.UpdateOperation,
		Path:      "role/testrole/secret-id",
		Storage:   s,
		Data: map[string]interface{}{
			"token_bound_cidrs": []string{"11.0.0.0/24"},
		},
	}
	resp, err = b.HandleRequest(context.Background(), roleSecretIDReq)
	if err == nil {
		t.Fatal("expected error due to mismatching subnet relationship")
	}

	roleSecretIDReq.Data["token_bound_cidrs"] = "10.0.0.0/24"
	resp = b.requestNoErr(t, roleSecretIDReq)

	secretID := resp.Data["secret_id"]

	resp = b.requestNoErr(t, &logical.Request{
		Path:      "login",
		Operation: logical.UpdateOperation,
		Data: map[string]interface{}{
			"role_id":   roleID,
			"secret_id": secretID,
		},
		Storage:    s,
		Connection: &logical.Connection{RemoteAddr: "127.0.0.1"},
	})

	if resp.Auth == nil {
		t.Fatal("expected login to succeed")
	}
	if len(resp.Auth.BoundCIDRs) != 1 {
		t.Fatal("bad token bound cidrs")
	}
	if resp.Auth.BoundCIDRs[0].String() != "10.0.0.0/24" {
		t.Fatalf("bad: %s", resp.Auth.BoundCIDRs[0].String())
	}
}

func TestAppRole_RoleLogin(t *testing.T) {
	var resp *logical.Response
	var err error
	b, storage := createBackendWithStorage(t)

	createRole(t, b, storage, "role1", "a,b,c")
	roleRoleIDReq := &logical.Request{
		Operation: logical.ReadOperation,
		Path:      "role/role1/role-id",
		Storage:   storage,
	}
	resp = b.requestNoErr(t, roleRoleIDReq)

	roleID := resp.Data["role_id"]

	roleSecretIDReq := &logical.Request{
		Operation: logical.UpdateOperation,
		Path:      "role/role1/secret-id",
		Storage:   storage,
	}
	resp = b.requestNoErr(t, roleSecretIDReq)

	secretID := resp.Data["secret_id"]

	loginData := map[string]interface{}{
		"role_id":   roleID,
		"secret_id": secretID,
	}
	loginReq := &logical.Request{
		Operation: logical.UpdateOperation,
		Path:      "login",
		Storage:   storage,
		Data:      loginData,
		Connection: &logical.Connection{
			RemoteAddr: "127.0.0.1",
		},
	}
	loginResp, err := b.HandleRequest(context.Background(), loginReq)
	if err != nil || (loginResp != nil && loginResp.IsError()) {
		t.Fatalf("err:%v resp:%#v", err, loginResp)
	}

	if loginResp.Auth == nil {
		t.Fatalf("expected a non-nil auth object in the response")
	}

	if loginResp.Auth.Metadata == nil {
		t.Fatalf("expected a non-nil metadata object in the response")
	}

	if val := loginResp.Auth.Metadata["role_name"]; val != "role1" {
		t.Fatalf("expected metadata.role_name to equal 'role1', got: %v", val)
	}

	if loginResp.Auth.Alias.Metadata == nil {
		t.Fatalf("expected a non-nil alias metadata object in the response")
	}

	if val := loginResp.Auth.Alias.Metadata["role_name"]; val != "role1" {
		t.Fatalf("expected metadata.alias.role_name to equal 'role1', got: %v", val)
	}

	// Test renewal
	renewReq := generateRenewRequest(storage, loginResp.Auth)

	resp, err = b.HandleRequest(context.Background(), renewReq)
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("err:%v resp:%#v", err, resp)
	}

	if resp.Auth.TTL != 400*time.Second {
		t.Fatalf("expected period value from response to be 400s, got: %s", resp.Auth.TTL)
	}

	///
	// Test renewal with period
	///

	// Create role
	period := 600 * time.Second
	roleData := map[string]interface{}{
		"policies": "a,b,c",
		"period":   period.String(),
	}
	roleReq := &logical.Request{
		Operation: logical.CreateOperation,
		Path:      "role/" + "role-period",
		Storage:   storage,
		Data:      roleData,
	}
	resp = b.requestNoErr(t, roleReq)

	roleRoleIDReq = &logical.Request{
		Operation: logical.ReadOperation,
		Path:      "role/role-period/role-id",
		Storage:   storage,
	}
	resp = b.requestNoErr(t, roleRoleIDReq)

	roleID = resp.Data["role_id"]

	roleSecretIDReq = &logical.Request{
		Operation: logical.UpdateOperation,
		Path:      "role/role-period/secret-id",
		Storage:   storage,
	}
	resp = b.requestNoErr(t, roleSecretIDReq)

	secretID = resp.Data["secret_id"]

	loginData["role_id"] = roleID
	loginData["secret_id"] = secretID

	loginResp, err = b.HandleRequest(context.Background(), loginReq)
	if err != nil || (loginResp != nil && loginResp.IsError()) {
		t.Fatalf("err:%v resp:%#v", err, loginResp)
	}

	if loginResp.Auth == nil {
		t.Fatalf("expected a non-nil auth object in the response")
	}

	renewReq = generateRenewRequest(storage, loginResp.Auth)

	resp, err = b.HandleRequest(context.Background(), renewReq)
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("err:%v resp:%#v", err, resp)
	}

	if resp.Auth.Period != period {
		t.Fatalf("expected period value of %d in the response, got: %s", period, resp.Auth.Period)
	}

	// Test input validation with secret_id that exceeds max length
	loginData["secret_id"] = strings.Repeat("a", maxHmacInputLength+1)

	loginReq = &logical.Request{
		Operation: logical.UpdateOperation,
		Path:      "login",
		Storage:   storage,
		Data:      loginData,
		Connection: &logical.Connection{
			RemoteAddr: "127.0.0.1",
		},
	}

	loginResp, err = b.HandleRequest(context.Background(), loginReq)

	expectedErr := "failed to create HMAC of secret_id"
	if loginResp != nil || err == nil || !strings.Contains(err.Error(), expectedErr) {
		t.Fatalf("expected login test to fail with error %q, resp: %#v, err: %v", expectedErr, loginResp, err)
	}
}

func generateRenewRequest(s logical.Storage, auth *logical.Auth) *logical.Request {
	renewReq := &logical.Request{
		Operation: logical.RenewOperation,
		Storage:   s,
		Auth:      &logical.Auth{},
	}
	renewReq.Auth.InternalData = auth.InternalData
	renewReq.Auth.Metadata = auth.Metadata
	renewReq.Auth.LeaseOptions = auth.LeaseOptions
	renewReq.Auth.Policies = auth.Policies
	renewReq.Auth.Period = auth.Period

	return renewReq
}

func TestAppRole_RoleResolve(t *testing.T) {
	b, storage := createBackendWithStorage(t)

	role := "role1"
	createRole(t, b, storage, role, "a,b,c")
	roleRoleIDReq := &logical.Request{
		Operation: logical.ReadOperation,
		Path:      "role/role1/role-id",
		Storage:   storage,
	}
	resp := b.requestNoErr(t, roleRoleIDReq)

	roleID := resp.Data["role_id"]

	roleSecretIDReq := &logical.Request{
		Operation: logical.UpdateOperation,
		Path:      "role/role1/secret-id",
		Storage:   storage,
	}
	resp = b.requestNoErr(t, roleSecretIDReq)

	secretID := resp.Data["secret_id"]

	loginData := map[string]interface{}{
		"role_id":   roleID,
		"secret_id": secretID,
	}
	loginReq := &logical.Request{
		Operation: logical.ResolveRoleOperation,
		Path:      "login",
		Storage:   storage,
		Data:      loginData,
		Connection: &logical.Connection{
			RemoteAddr: "127.0.0.1",
		},
	}

	resp = b.requestNoErr(t, loginReq)

	if resp.Data["role"] != role {
		t.Fatalf("Role was not as expected. Expected %s, received %s", role, resp.Data["role"])
	}
}

func TestAppRole_RoleDoesNotExist(t *testing.T) {
	var resp *logical.Response
	var err error
	b, storage := createBackendWithStorage(t)

	roleID := "roleDoesNotExist"

	loginData := map[string]interface{}{
		"role_id":   roleID,
		"secret_id": "secret",
	}
	loginReq := &logical.Request{
		Operation: logical.ResolveRoleOperation,
		Path:      "login",
		Storage:   storage,
		Data:      loginData,
		Connection: &logical.Connection{
			RemoteAddr: "127.0.0.1",
		},
	}

	resp, err = b.HandleRequest(context.Background(), loginReq)
	if resp == nil && !resp.IsError() {
		t.Fatalf("Response was not an error: err:%v resp:%#v", err, resp)
	}

	errString, ok := resp.Data["error"].(string)
	if !ok {
		t.Fatal("Error not part of response.")
	}

	if !strings.Contains(errString, "invalid role or secret ID") {
		t.Fatalf("Error was not due to invalid role ID. Error: %s", errString)
	}
}
