Skip to content

Commit

Permalink
Feature/17 implement service accounts (#23)
Browse files Browse the repository at this point in the history
* chore(serviceaccount): List and Create service accounts

* chore(serviceaccount): Add service to Keyhub class

* Added update, get and delete functions

* chore(serviceaccount): Add ServiceAccountQueryParams

* chore(test): add some queryParam marshal testing

* chore(serviceaccounts): Added remaining query params
  • Loading branch information
maikelpoot authored Oct 17, 2023
1 parent 0b64ad7 commit 7dad85b
Show file tree
Hide file tree
Showing 5 changed files with 362 additions and 0 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Hotfix: Fix undetected parse error for vault records without an enddate

### Added
- Issue #17 : Implement Service Accounts

## [1.2.4] - 2023-02-23
### Changed
- Issue #18 : Improved error reporting by returning the ErrorReport
Expand Down
2 changes: 2 additions & 0 deletions keyhub.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ type Client struct {
Systems *SystemService
ClientApplications *ClientApplicationService
Vaults *VaultService
ServiceAccounts *ServiceAccountService
LaunchPadTile *LaunchPadTileService
}

Expand Down Expand Up @@ -145,5 +146,6 @@ func NewClient(httpClient *http.Client, issuer string, clientID string, clientSe
Systems: newSystemService(oauth2Sling.New()),
LaunchPadTile: newLaunchPadTileService(oauth2Sling.New()),
Vaults: newVaultService(versionedSling.New().Client(vaultClient)),
ServiceAccounts: NewServiceAccountService(oauth2Sling),
}, nil
}
56 changes: 56 additions & 0 deletions keyhub_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@
package keyhub

import (
"github.com/google/go-querystring/query"
"net/http"
"strconv"
"testing"
"time"

"github.com/google/uuid"
"github.com/jarcoal/httpmock"
Expand Down Expand Up @@ -132,3 +134,57 @@ func TestGroups(t *testing.T) {
t.Fatalf("ERROR group with id 1 not found")
}
}

func verifyQueryParams(t *testing.T, queryParams interface{}, expected string) {

r, err := query.Values(queryParams)
if err != nil {
t.Fatalf("Could not parse query params")
}

result := r.Encode()

if result != expected {
t.Fatalf("Parse error, result `%s` did not match expected `%s`", result, expected)
}

}

func TestQueries(t *testing.T) {

var q model.ServiceAccountQueryParams

q = model.ServiceAccountQueryParams{}
verifyQueryParams(t, q, "")

CreatedAfter, _ := time.Parse("2006-01-02", "2023-01-04")
CreatedBefore, _ := time.Parse("2006-01-02", "2023-01-04")
ModifiedSince, _ := time.Parse("2006-01-02", "2023-01-04")

q = model.ServiceAccountQueryParams{
UUID: "51f0cb1d-5745-4512-8d0d-bb28e2449d3f",
CreatedAfter: CreatedAfter,
CreatedBefore: CreatedBefore,
ModifiedSince: ModifiedSince,
Additional: nil,
Exclude: []int64{1000},
Id: []int64{1001},
CQLQuery: "Blaat",
Active: "BOTH",
GroupOnSystem: 1002,
GroupOnSystemOwners: 1003,
Name: "Name",
NameContains: "Contains",
NameDoesNotStartWith: "NotStartWith",
NameStartsWith: "StartsWith",
Password: 1004,
PasswordRotation: "MANUAL",
RequestedGroupOnSystemOwners: 1005,
System: 1006,
TechnicalAdministrator: 1007,
Username: "Username",
}

verifyQueryParams(t, q, "Username=Username&active=BOTH&createdAfter=2023-01-04T00%3A00%3A00Z&createdBefore=2023-01-04T00%3A00%3A00Z&createdBefore=2023-01-04T00%3A00%3A00Z&exclude=1000&groupOnSystem=1002&groupOnSystemOwners=1003&id=1001&name=Name&nameContains=Contains&nameDoesNotStartWith=NotStartWith&nameStartsWith=StartsWith&password=1004&passwordRotation=MANUAL&q=Blaat&requestedGroupOnSystemOwners=1005&system=1006&technicalAdministrator=1007&uuid=51f0cb1d-5745-4512-8d0d-bb28e2449d3f")

}
106 changes: 106 additions & 0 deletions model/serviceaccount.go
Original file line number Diff line number Diff line change
@@ -1 +1,107 @@
package model

import (
"github.com/google/uuid"
"net/url"
"time"
)

const (
SA_PASSWORD_ROTATION_MANUAL SAPasswordRotation = "MANUAL"
SA_PASSWORD_ROTATION_MANUAL_SIV SAPasswordRotation = "MANUAL_STORED_IN_VAULT"
SA_PASSWORD_ROTATION_DAILY SAPasswordRotation = "DAILY"
)

type SAPasswordRotation string

type ServiceAccountList struct {
Items []ServiceAccount `json:"items"`
}

type ServiceAccountPrimer struct {
//#/components/schemas/serviceaccount_ServiceAccountPrimer
//serviceaccount_ServiceAccount

Linkable

Active bool `json:"active"`
UUID uuid.UUID `json:"uuid,omitempty"`
Name string `json:"name"`
Username string `json:"username"`
System *ProvisionedSystem `json:"system"`
}

type ServiceAccount struct {
ServiceAccountPrimer

TechnicalAdministrator *Group `json:"technicalAdministrator,omitempty"`
PasswordRotation SAPasswordRotation `json:"passwordRotation"`
Description string `json:"description,omitempty"`
Password *VaultRecord `json:"password,omitempty"`
}

// AsPrimer Return ServiceAccount with only Primer data
func (s *ServiceAccount) AsPrimer() *ServiceAccount {
serviceAccount := &ServiceAccount{}
serviceAccount.ServiceAccountPrimer = s.ServiceAccountPrimer
return serviceAccount
}

// ToPrimer Convert to serviceAccountPrimer
func (s *ServiceAccount) ToPrimer() *ServiceAccountPrimer {
serviceAccountPrimer := s.ServiceAccountPrimer
return &serviceAccountPrimer
}

type ServiceAccountGroupList struct {
Items []ServiceAccountGroup `json:"items"`
}

type ServiceAccountGroup struct {
GroupOnSystem
}

type ServiceAccountQueryParams struct {
UUID string `url:"uuid,omitempty"`
CreatedAfter time.Time `url:"createdAfter,omitempty" layout:"2006-01-02T15:04:05Z"`
CreatedBefore time.Time `url:"createdBefore,omitempty" layout:"2006-01-02T15:04:05Z"`
ModifiedSince time.Time `url:"createdBefore,omitempty" layout:"2006-01-02T15:04:05Z"`
Additional *ServiceAccountAdditionalQueryParams `url:"additional,omitempty"`
Exclude []int64 `url:"exclude,omitempty"`
Id []int64 `url:"id,omitempty"`
CQLQuery string `url:"q,omitempty"`
Active string `url:"active,omitempty"`
GroupOnSystem int64 `url:"groupOnSystem,omitempty"`
GroupOnSystemOwners int64 `url:"groupOnSystemOwners,omitempty"`
Name string `url:"name,omitempty"`
NameContains string `url:"nameContains,omitempty"`
NameDoesNotStartWith string `url:"nameDoesNotStartWith,omitempty"`
NameStartsWith string `url:"nameStartsWith,omitempty"`
Password int64 `url:"password,omitempty"`
PasswordRotation string `url:"passwordRotation,omitempty"`
RequestedGroupOnSystemOwners int64 `url:"requestedGroupOnSystemOwners,omitempty"`
System int64 `url:"system,omitempty"`
TechnicalAdministrator int64 `url:"technicalAdministrator,omitempty"`
Username string `url:"Username,omitempty"`
}

type ServiceAccountAdditionalQueryParams struct {
Audit bool `url:"audit"`
Groups bool `url:"groups"`
Secrets bool `url:"secrets"`
}

// EncodeValues Custom url encoder to convert bools to list
func (p ServiceAccountAdditionalQueryParams) EncodeValues(key string, v *url.Values) error {
return additionalQueryParamsUrlEncoder(p, key, v)
}

func NewServiceAccount() *ServiceAccount {

return &ServiceAccount{
ServiceAccountPrimer: ServiceAccountPrimer{
Linkable: Linkable{DType: "serviceaccount.ServiceAccount"},
},
PasswordRotation: SA_PASSWORD_ROTATION_MANUAL,
}
}
195 changes: 195 additions & 0 deletions serviceaccounts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
/* Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You 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 keyhub

import (
"fmt"
"github.com/dghubble/sling"
"github.com/google/uuid"
"github.com/topicuskeyhub/go-keyhub/model"
"net/http"
"net/url"
"strconv"
)

type ServiceAccountService struct {
sling *sling.Sling
}

// NewServiceAccountService Create new ServiceAccountService
func NewServiceAccountService(sling *sling.Sling) *ServiceAccountService {
return &ServiceAccountService{
sling: sling.Path("/keyhub/rest/v1/serviceaccount/"),
}
}

// GetByUUID Get Service account by UUID
func (s *ServiceAccountService) GetByUUID(uuid uuid.UUID) (result *model.ServiceAccount, err error) {
list := new(model.ServiceAccountList)
errorReport := new(model.ErrorReport)

params := &model.ServiceAccountQueryParams{UUID: uuid.String()}

_, err = s.sling.New().Get("").QueryStruct(params).Receive(list, errorReport)
if errorReport.Code > 0 {
err = errorReport.Wrap("Could not get ServiceAccount %q.", uuid)
}
if err == nil {
if len(list.Items) > 0 {
result = &list.Items[0]
} else {
err = fmt.Errorf("Account %q not found", uuid.String())
}
}

return
}

// GetById Get Service account by ID
func (s *ServiceAccountService) GetById(id int64) (result *model.ServiceAccount, err error) {
sa := new(model.ServiceAccount)
errorReport := new(model.ErrorReport)
idString := strconv.FormatInt(id, 10)

_, err = s.sling.New().Get(idString).Receive(sa, errorReport)
if errorReport.Code > 0 {
err = errorReport.Wrap("Could not get ServiceAccount %q.", idString)
return
}
if err == nil && sa == nil {
err = fmt.Errorf("Account %q not found", idString)
return
}

//use an intermediate variable so sling can fill that variable with the json results. When request was succesful we use the variable as return value.
result = sa
return
}

//// List Retrieve all vault records for a group (secrets are not included, default audit = true)
//func (s *VaultService) List(group *model.Group, query *model.VaultRecordQueryParams, additional *model.VaultRecordAdditionalQueryParams) (records []model.VaultRecord, err error) {

// List all Service Accounts
func (s *ServiceAccountService) List(query *model.ServiceAccountQueryParams, additional *model.ServiceAccountAdditionalQueryParams) (list []model.ServiceAccount, err error) {
list = []model.ServiceAccount{}

if query == nil {
query = new(model.ServiceAccountQueryParams)
}
if additional != nil {
query.Additional = additional
}

searchRange := model.NewRange()

var response *http.Response

for ok := true; ok; ok = searchRange.NextPage() {

errorReport := new(model.ErrorReport)
results := new(model.ServiceAccountList)
response, err = s.sling.New().Get("").QueryStruct(query).Add(searchRange.GetRequestRangeHeader()).Add(searchRange.GetRequestModeHeader()).Receive(results, errorReport)
searchRange.ParseResponse(response)

if errorReport.Code > 0 {
err = errorReport.Wrap("Could not get Groups.")
}
if err == nil {
if len(results.Items) > 0 {
list = append(list, results.Items...)
}
}

}

return
}

// Create Create a serviceaccount
func (s *ServiceAccountService) Create(serviceaccount *model.ServiceAccount) (result *model.ServiceAccount, err error) {
serviceAccounts := new(model.ServiceAccountList)
results := new(model.ServiceAccountList)
errorReport := new(model.ErrorReport)
serviceAccounts.Items = append(serviceAccounts.Items, *serviceaccount)

_, err = s.sling.New().Post("").BodyJSON(serviceAccounts).Receive(results, errorReport)

if errorReport.Code > 0 {
fmt.Println("Wrap", errorReport.Message)
//apiErr := model.NewKeyhubApiError(*errorReport, "Could not create ServiceAccount in System %q.", serviceaccount.System.Name)
err = errorReport.Wrap("Could not create ServiceAccount in System %q.", serviceaccount.System.Name)

return
}
if err == nil {
if len(results.Items) > 0 {
result = &results.Items[0]
} else {
err = fmt.Errorf("Could not create ServiceAccount")
}
}

return
}

// Update Update service account
func (s *ServiceAccountService) Update(serviceAccount *model.ServiceAccount) (result *model.ServiceAccount, err error) {
updated := new(model.ServiceAccount)
errorReport := new(model.ErrorReport)

selfUrl, _ := url.Parse(serviceAccount.Self().Href)

_, err = s.sling.New().Path(selfUrl.Path).Put("").BodyJSON(serviceAccount).Receive(updated, errorReport)
if errorReport.Code > 0 {
err = errorReport.Wrap("Could not update ServiceAccount %s", serviceAccount.UUID)
return
}
result = updated
return
}

// Delete Delete a service account by object
func (s *ServiceAccountService) Delete(serviceAccount *model.ServiceAccount) (err error) {
errorReport := new(model.ErrorReport)

selfUrl, _ := url.Parse(serviceAccount.Self().Href)

_, err = s.sling.New().Path(selfUrl.Path).Delete("").Receive(nil, errorReport)
if errorReport.Code > 0 {
err = errorReport.Wrap("Could not delete ServiceAccount %q", serviceAccount.UUID)
}

return
}

// DeleteByUUID Delete a service account by uuid for a certain group
func (s *ServiceAccountService) DeleteByUUID(uuid uuid.UUID) (err error) {
serviceAccount, err := s.GetByUUID(uuid)
if err != nil {
return err
}

return s.Delete(serviceAccount)
}

// DeleteByID Delete a service account by ID
func (s *ServiceAccountService) DeleteByID(id int64) (err error) {
serviceAccount, err := s.GetById(id)
if err != nil {
return err
}
return s.Delete(serviceAccount)
}

0 comments on commit 7dad85b

Please sign in to comment.