Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: initial commit #2

Merged
merged 3 commits into from
Feb 23, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 184 additions & 0 deletions api/authentication/authentication_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
// Copyright 2024 BeyondTrust. All rights reserved.
// Package authentication implements functions to call Beyondtrust Secret Safe API.
// Unit tests for authentication package.
package authentication

import (
"go-client-library-passwordsafe/api/entities"
"go-client-library-passwordsafe/api/logging"
"go-client-library-passwordsafe/api/utils"
"reflect"

"net/http"
"net/http/httptest"
"testing"

"go.uber.org/zap"
)

type UserTestConfig struct {
name string
server *httptest.Server
response *entities.SignApinResponse
}

type GetTokenConfig struct {
name string
server *httptest.Server
response string
}

type GetPasswordSafeAuthenticationConfig struct {
name string
server *httptest.Server
response *entities.SignApinResponse
}

func TestSignOut(t *testing.T) {
logger, _ := zap.NewDevelopment()

// create a zap logger wrapper
zapLogger := logging.NewZapLogger(logger)

httpClientObj, _ := utils.GetHttpClient(5, false, "", "", zapLogger)

var authenticate, _ = Authenticate(*httpClientObj, "https://fake.api.com:443/BeyondTrust/api/public/v3/", "fakeone_a654+9sdf7+8we4f", "fakeone_aasd156465sfdef", zapLogger, 300)
testConfig := UserTestConfig{
name: "TestSignOut",
server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, err := w.Write([]byte(``))
if err != nil {
t.Error("Test case Failed")
}

})),
response: nil,
}

err := authenticate.SignOut(testConfig.server.URL)
if err != nil {
t.Errorf("Test case Failed: %v", err)
}
}

func TestSignAppin(t *testing.T) {
logger, _ := zap.NewDevelopment()

// create a zap logger wrapper
zapLogger := logging.NewZapLogger(logger)

httpClientObj, _ := utils.GetHttpClient(5, false, "", "", zapLogger)

var authenticate, _ = Authenticate(*httpClientObj, "https://fake.api.com:443/BeyondTrust/api/public/v3/", "fakeone_a654+9sdf7+8we4f", "fakeone_aasd156465sfdef", zapLogger, 300)
testConfig := UserTestConfig{
name: "TestSignAppin",
server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, err := w.Write([]byte(`{"UserId":1, "EmailAddress":"Felipe"}`))
if err != nil {
t.Error("Test case Failed")
}
})),
response: &entities.SignApinResponse{
UserId: 1,
EmailAddress: "Felipe",
},
}

response, err := authenticate.SignAppin(testConfig.server.URL+"/"+"TestSignAppin", "")

if !reflect.DeepEqual(response, *testConfig.response) {
t.Errorf("Test case Failed %v, %v", response, *testConfig.response)
}

if err != nil {
t.Errorf("Test case Failed: %v", err)
}
}

func TestGetToken(t *testing.T) {
logger, _ := zap.NewDevelopment()

// create a zap logger wrapper
zapLogger := logging.NewZapLogger(logger)

httpClientObj, _ := utils.GetHttpClient(5, false, "", "", zapLogger)

var authenticate, _ = Authenticate(*httpClientObj, "https://fake.api.com:443/BeyondTrust/api/public/v3/", "fakeone_a654+9sdf7+8we4f", "fakeone_aasd156465sfdef", zapLogger, 300)
testConfig := GetTokenConfig{
name: "TestGetToken",
server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Mocking Response accorging to the endpoint path
thejurysays marked this conversation as resolved.
Show resolved Hide resolved
switch r.URL.Path {

case "/Auth/connect/token":
_, err := w.Write([]byte(`{"access_token": "fake_token", "expires_in": 600, "token_type": "Bearer", "scope": "publicapi"}`))
if err != nil {
t.Error("Test case Failed")
}

default:
http.NotFound(w, r)
}
})),
response: "fake_token",
}

response, err := authenticate.GetToken(testConfig.server.URL+"/"+"Auth/connect/token", "", "")

if response != testConfig.response {
t.Errorf("Test case Failed %v, %v", response, testConfig.response)
}

if err != nil {
t.Errorf("Test case Failed: %v", err)
}
}

func TestGetPasswordSafeAuthentication(t *testing.T) {
logger, _ := zap.NewDevelopment()

// create a zap logger wrapper
zapLogger := logging.NewZapLogger(logger)

httpClientObj, _ := utils.GetHttpClient(5, false, "", "", zapLogger)

var authenticate, _ = Authenticate(*httpClientObj, "https://fake.api.com:443/BeyondTrust/api/public/v3/", "fakeone_a654+9sdf7+8we4f", "fakeone_aasd156465sfdef", zapLogger, 300)
testConfig := GetPasswordSafeAuthenticationConfig{
name: "TestGetPasswordSafeAuthentication",
server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Mocking Response according to the endpoint path
switch r.URL.Path {

case "/Auth/connect/token":
_, err := w.Write([]byte(`{"access_token": "fake_token", "expires_in": 600, "token_type": "Bearer", "scope": "publicapi"}`))
if err != nil {
t.Error("Test case Failed")
}

case "/Auth/SignAppIn":
_, err := w.Write([]byte(`{"UserId":1, "EmailAddress":"Felipe"}`))

if err != nil {
t.Error("Test case Failed")
}

default:
http.NotFound(w, r)
}
})),
response: &entities.SignApinResponse{
UserId: 1,
EmailAddress: "Felipe",
},
}
authenticate.ApiUrl = testConfig.server.URL + "/"
response, err := authenticate.GetPasswordSafeAuthentication()

if !reflect.DeepEqual(response, *testConfig.response) {
t.Errorf("Test case Failed %v, %v", response, *testConfig.response)
}

if err != nil {
t.Errorf("Test case Failed: %v", err)
}
}
182 changes: 182 additions & 0 deletions api/authentication/authetication.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
// Copyright 2024 BeyondTrust. All rights reserved.
// Package client implements functions to call Beyondtrust Secret Safe API.
package authentication

import (
"bytes"
"encoding/json"
"fmt"
"go-client-library-passwordsafe/api/entities"
"go-client-library-passwordsafe/api/logging"
"go-client-library-passwordsafe/api/utils"
"io"

"net/url"
"time"

backoff "github.com/cenkalti/backoff/v4"
)

type AuthenticationObj struct {
ApiUrl string
clientId string
clientSecret string
HttpClient utils.HttpClientObj
ExponentialBackOff *backoff.ExponentialBackOff
log logging.Logger
}

// Authenticate in PS API
thejurysays marked this conversation as resolved.
Show resolved Hide resolved
func Authenticate(httpClient utils.HttpClientObj, endpointUrl string, clientId string, clientSecret string, logger logging.Logger, retryMaxElapsedTimeSeconds int) (*AuthenticationObj, error) {

backoffDefinition := backoff.NewExponentialBackOff()
backoffDefinition.InitialInterval = 1 * time.Second
backoffDefinition.MaxElapsedTime = time.Duration(retryMaxElapsedTimeSeconds) * time.Second
backoffDefinition.RandomizationFactor = 0.5

// Client
thejurysays marked this conversation as resolved.
Show resolved Hide resolved
var client = httpClient

authenticationObj := &AuthenticationObj{
ApiUrl: endpointUrl,
HttpClient: client,
clientId: clientId,
clientSecret: clientSecret,
ExponentialBackOff: backoffDefinition,
log: logger,
}

return authenticationObj, nil
}

// GetPasswordSafeAuthentication call get token and sign app endpoint
thejurysays marked this conversation as resolved.
Show resolved Hide resolved
func (authenticationObj *AuthenticationObj) GetPasswordSafeAuthentication() (entities.SignApinResponse, error) {
accessToken, err := authenticationObj.GetToken(fmt.Sprintf("%v%v", authenticationObj.ApiUrl, "Auth/connect/token"), authenticationObj.clientId, authenticationObj.clientSecret)
if err != nil {
return entities.SignApinResponse{}, err
}
signApinResponse, err := authenticationObj.SignAppin(fmt.Sprintf("%v%v", authenticationObj.ApiUrl, "Auth/SignAppIn"), accessToken)
if err != nil {
return entities.SignApinResponse{}, err
}
return signApinResponse, nil
}

// GetToken get token from PS API
thejurysays marked this conversation as resolved.
Show resolved Hide resolved
func (authenticationObj *AuthenticationObj) GetToken(endpointUrl string, clientId string, clientSecret string) (string, error) {

params := url.Values{}
params.Add("client_id", clientId)
params.Add("client_secret", clientSecret)
params.Add("grant_type", "client_credentials")

var body io.ReadCloser
var technicalError error
var businessError error

var buffer bytes.Buffer
buffer.WriteString(params.Encode())

technicalError = backoff.Retry(func() error {
body, technicalError, businessError, _ = authenticationObj.HttpClient.CallSecretSafeAPI(endpointUrl, "POST", buffer, "GetToken", "")
return technicalError
}, authenticationObj.ExponentialBackOff)

if technicalError != nil {
return "", technicalError
}

if businessError != nil {
return "", businessError
}

defer body.Close()
bodyBytes, err := io.ReadAll(body)

if err != nil {
return "", err
}

responseString := string(bodyBytes)

var data entities.GetTokenResponse

err = json.Unmarshal([]byte(responseString), &data)
if err != nil {
authenticationObj.log.Error(err.Error())
return "", err
}

thejurysays marked this conversation as resolved.
Show resolved Hide resolved
return data.AccessToken, nil

}

// SignAppin Signs app in PS API
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// SignAppin is responsible for creating a PS API session.

func (authenticationObj *AuthenticationObj) SignAppin(endpointUrl string, accessToken string) (entities.SignApinResponse, error) {

var userObject entities.SignApinResponse
var body io.ReadCloser
var technicalError error
var businessError error
var scode int

err := backoff.Retry(func() error {
body, technicalError, businessError, scode = authenticationObj.HttpClient.CallSecretSafeAPI(endpointUrl, "POST", bytes.Buffer{}, "SignAppin", accessToken)
if scode == 0 {
return nil
}
return technicalError
}, authenticationObj.ExponentialBackOff)

if err != nil {
return entities.SignApinResponse{}, err
}

if scode == 0 {
return entities.SignApinResponse{}, technicalError
}

if businessError != nil {
return entities.SignApinResponse{}, businessError
}

defer body.Close()
bodyBytes, err := io.ReadAll(body)
if err != nil {
return entities.SignApinResponse{}, err
}

err = json.Unmarshal(bodyBytes, &userObject)

if err != nil {
authenticationObj.log.Error(err.Error())
return entities.SignApinResponse{}, err
}

thejurysays marked this conversation as resolved.
Show resolved Hide resolved
return userObject, nil
}

// SignOut signs out Secret Safe API.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// SignOut is responsible for closing the PS API session and cleaning up idle connections.

// Warn: should only be called one time for all data sources.
func (authenticationObj *AuthenticationObj) SignOut(url string) error {
authenticationObj.log.Debug(url)

var technicalError error
var businessError error
var body io.ReadCloser

technicalError = backoff.Retry(func() error {
body, technicalError, businessError, _ = authenticationObj.HttpClient.CallSecretSafeAPI(url, "POST", bytes.Buffer{}, "SignOut", "")
return technicalError
}, authenticationObj.ExponentialBackOff)

thejurysays marked this conversation as resolved.
Show resolved Hide resolved
defer body.Close()
if businessError != nil {
authenticationObj.log.Error(businessError.Error())
return businessError
}

defer authenticationObj.HttpClient.HttpClient.CloseIdleConnections()

thejurysays marked this conversation as resolved.
Show resolved Hide resolved
return nil
}
29 changes: 29 additions & 0 deletions api/entities/entities.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright 2024 BeyondTrust. All rights reserved.
// Package entities implements DTO's used by Beyondtrust Secret Safe API.
package entities

type SignApinResponse struct {
UserId int `json:"UserId"`
EmailAddress string `json:"EmailAddress"`
UserName string `json:"UserName"`
Name string `json:"Name"`
}

type ManagedAccount struct {
SystemId int
AccountId int
}

type Secret struct {
Id string
Title string
Password string
SecretType string
}

type GetTokenResponse struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
TokenType string `json:"token_type"`
Scope string `json:"scope"`
}
Loading
Loading