Skip to content

Commit

Permalink
Add AccountManagementDriver
Browse files Browse the repository at this point in the history
  • Loading branch information
SuperSandro2000 committed Mar 22, 2024
1 parent 711dce8 commit cfeabb4
Show file tree
Hide file tree
Showing 11 changed files with 337 additions and 35 deletions.
142 changes: 142 additions & 0 deletions internal/drivers/basic/account_management.go
@@ -0,0 +1,142 @@
/******************************************************************************
*
* Copyright 2024 SAP SE
*
* Licensed 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 basic

import (
"encoding/json"
"errors"
"fmt"
"os"
"strings"

"github.com/sapcc/keppel/internal/keppel"

"gopkg.in/yaml.v2"
)

// AccountManagementDriver is the account management driver "basic".
type AccountManagementDriver struct {
configPath string
config AccountConfig
}

type AccountConfig struct {
Accounts []Accounts `yaml:"accounts"`
}

type Accounts struct {
Name string `yaml:"name"`
AuthTenantID string `yaml:"auth_tenant_id"`
GCPolicies []keppel.GCPolicy `yaml:"gc_policies"`
RBACPolicies []keppel.RBACPolicy `yaml:"rbac_policies"`
ReplicationPolicy keppel.ReplicationPolicy `yaml:"replication_policy"`
SecurityScanPolicies []keppel.SecurityScanPolicy `yaml:"security_scan_policies"`
ValidationPolicy keppel.ValidationPolicy `yaml:"validation_policy"`
}

func init() {
keppel.AccountManagementDriverRegistry.Add(func() keppel.AccountManagementDriver {
return &AccountManagementDriver{}
})
}

// PluginTypeID implements the keppel.AccountManagementDriver interface.
func (a *AccountManagementDriver) PluginTypeID() string { return "basic" }

// ConfigureAccount implements the keppel.AccountManagementDriver interface.
func (a *AccountManagementDriver) Init(envToConfigFile string) error {
a.configPath = os.Getenv("KEPPEL_ACCOUNT_MANAGEMENT_FILE")
if a.configPath == "" {
return errors.New("KEPPEL_ACCOUNT_MANAGEMENT_FILE is not set")
}

return a.loadConfig()
}

// ConfigureAccount implements the keppel.AccountManagementDriver interface.
func (a *AccountManagementDriver) ConfigureAccount(db *keppel.DB, account keppel.Account) (keppel.Account, error) {
for _, cfgAccount := range a.config.Accounts {
if cfgAccount.AuthTenantID != account.AuthTenantID {
continue
}

account.IsManaged = true
account.RequiredLabels = strings.Join(cfgAccount.ValidationPolicy.RequiredLabels, ",")

gcPolicyJSON, err := json.Marshal(cfgAccount.GCPolicies)
if err != nil {
return keppel.Account{}, fmt.Errorf("gc_policies: %w", err)
}
account.GCPoliciesJSON = string(gcPolicyJSON)

rbacPolicyJSON, err := json.Marshal(cfgAccount.RBACPolicies)
if err != nil {
return keppel.Account{}, fmt.Errorf("rbac_policies: %w", err)
}
account.RBACPoliciesJSON = string(rbacPolicyJSON)

securityScanPoliciesJSON, err := json.Marshal(cfgAccount.SecurityScanPolicies)
if err != nil {
return keppel.Account{}, fmt.Errorf("security_scan_policies: %w", err)
}
account.SecurityScanPoliciesJSON = string(securityScanPoliciesJSON)

_, err = cfgAccount.ReplicationPolicy.ApplyToAccount(db, &account)
if err != nil {
return keppel.Account{}, fmt.Errorf("replication_policy: %w", err)
}

return account, nil
}

// we didn't find the account, delete it
return keppel.Account{}, nil
}

func (a *AccountManagementDriver) ManagedAccountNames() ([]string, error) {
err := a.loadConfig()
if err != nil {
return nil, err
}

var accounts []string
for _, account := range a.config.Accounts {
accounts = append(accounts, account.Name)
}

return accounts, nil
}

func (a *AccountManagementDriver) loadConfig() error {
reader, err := os.Open(a.configPath)
if err != nil {
return err
}

decoder := yaml.NewDecoder(reader)
decoder.SetStrict(true)
var config AccountConfig
err = decoder.Decode(&config)
if err != nil {
return err
}

a.config = config
return nil
}
61 changes: 61 additions & 0 deletions internal/drivers/basic/account_management_test.go
@@ -0,0 +1,61 @@
/******************************************************************************
*
* Copyright 2024 SAP SE
*
* Licensed 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 basic

import (
"testing"

"github.com/sapcc/keppel/internal/keppel"

"github.com/sapcc/go-bits/assert"
)

func TestAccountManagementDriver(t *testing.T) {
driver := AccountManagementDriver{
configPath: "./fixtures/account_management.yaml",
}

listOfAccounts, err := driver.ManagedAccountNames()
if err != nil {
t.Fatalf(err.Error())
}
assert.DeepEqual(t, "account", listOfAccounts, []string{"abcde"})

account := keppel.Account{
IsManaged: true,
Name: "abcde",
AuthTenantID: "1245",
}
newAccount, err := driver.ConfigureAccount(nil, account)
if err != nil {
t.Fatalf(err.Error())
}

expectedAccount := keppel.Account{
Name: "abcde",
AuthTenantID: "1245",
ExternalPeerURL: "registry-tertiary.example.org",
RequiredLabels: "important-label,some-label",
IsManaged: true,
RBACPoliciesJSON: "[{\"match_repository\":\"library/.*\",\"permissions\":[\"anonymous_pull\"]},{\"match_repository\":\"library/alpine\",\"match_username\":\".*@tenant2\",\"permissions\":[\"pull\",\"push\"]}]",
GCPoliciesJSON: "[{\"match_repository\":\".*/database\",\"except_repository\":\"archive/.*\",\"time_constraint\":{\"on\":\"pushed_at\",\"newer_than\":{\"value\":6,\"unit\":\"h\"}},\"action\":\"protect\"},{\"match_repository\":\".*\",\"only_untagged\":true,\"action\":\"delete\"}]",
SecurityScanPoliciesJSON: "[{\"match_repository\":\".*\",\"match_vulnerability_id\":\".*\",\"except_fix_released\":true,\"action\":{\"assessment\":\"risk accepted: vulnerabilities without an available fix are not actionable\",\"ignore\":true}}]",
}
assert.DeepEqual(t, "account", newAccount, expectedAccount)
}
38 changes: 38 additions & 0 deletions internal/drivers/basic/fixtures/account_management.yaml
@@ -0,0 +1,38 @@
accounts:
- name: abcde
auth_tenant_id: "1245"
gc_policies:
- match_repository: .*/database
except_repository: archive/.*
time_constraint:
"on": pushed_at
newer_than: 21600000000000
action: protect
- match_repository: .*
only_untagged: true
action: delete
rbac_policies:
- match_repository: library/.*
permissions:
- anonymous_pull
- match_repository: library/alpine
match_username: .*@tenant2
permissions:
- pull
- push
replication_policy:
strategy: from_external_on_first_use
upstream_peer_hostname: ""
external_peer:
url: registry-tertiary.example.org
security_scan_policies:
- match_repository: .*
match_vulnerability_id: .*
except_fix_released: true
action:
assessment: 'risk accepted: vulnerabilities without an available fix are not actionable'
ignore: true
validation_policy:
required_labels:
- important-label
- some-label
51 changes: 51 additions & 0 deletions internal/keppel/account_management_driver.go
@@ -0,0 +1,51 @@
/******************************************************************************
*
* Copyright 2024 SAP SE
*
* Licensed 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 keppel

import (
"github.com/sapcc/go-bits/pluggable"
)

// AccountManagementDriver is a pluggable interface for receiving account
// configuration from an external system. Accounts can either be managed by
// this driver, or created and maintained by users through the Keppel API.
type AccountManagementDriver interface {
pluggable.Plugin
// Init is called before any other interface methods, and allows the plugin to
// perform first-time initialization.
Init(string) error

// Called by a jobloop for every account every once in a while (e.g. every hour).
//
// Returns an updated account object if the account is managed.
// The jobloop will apply the account in the DB accordingly.
//
// Returns Account{} if the account was managed, and now shall be deleted.
// The jobloop will clean up the manifests, blobs, repos and the account.
ConfigureAccount(*DB, Account) (Account, error)

// Called by a jobloop every once in a while (e.g. every hour).
//
// If new names appear in the list, the jobloop will create the
// respective accounts as configured by ConfigureAccount().
ManagedAccountNames() ([]string, error)
}

// AccountManagementDriverRegistry is a pluggable.Registry for AccountManagementDriver implementations.
var AccountManagementDriverRegistry pluggable.Registry[AccountManagementDriver]
8 changes: 8 additions & 0 deletions internal/keppel/database.go
Expand Up @@ -249,6 +249,14 @@ var sqlMigrations = map[string]string{
PRIMARY KEY (account_name, match_cidr, match_repository, match_username)
);
`,
"038_add_accounts_is_managed.up.sql": `
ALTER TABLE accounts
ADD COLUMN is_managed BOOLEAN NOT NULL DEFAULT FALSE;
`,
"038_add_accounts_is_managed.down.sql": `
ALTER TABLE accounts
DROP COLUMN is_managed;
`,
}

// DB adds convenience functions on top of gorp.DbMap.
Expand Down
24 changes: 12 additions & 12 deletions internal/keppel/gc_policy.go
Expand Up @@ -33,22 +33,22 @@ import (
// GCPolicy is a policy enabling optional garbage collection runs in an account.
// It is stored in serialized form in the GCPoliciesJSON field of type Account.
type GCPolicy struct {
RepositoryRx regexpext.BoundedRegexp `json:"match_repository"`
NegativeRepositoryRx regexpext.BoundedRegexp `json:"except_repository,omitempty"`
TagRx regexpext.BoundedRegexp `json:"match_tag,omitempty"`
NegativeTagRx regexpext.BoundedRegexp `json:"except_tag,omitempty"`
OnlyUntagged bool `json:"only_untagged,omitempty"`
TimeConstraint *GCTimeConstraint `json:"time_constraint,omitempty"`
Action string `json:"action"`
RepositoryRx regexpext.BoundedRegexp `json:"match_repository" yaml:"match_repository"`
NegativeRepositoryRx regexpext.BoundedRegexp `json:"except_repository,omitempty" yaml:"except_repository,omitempty"`
TagRx regexpext.BoundedRegexp `json:"match_tag,omitempty" yaml:"match_tag,omitempty"`
NegativeTagRx regexpext.BoundedRegexp `json:"except_tag,omitempty" yaml:"except_tag,omitempty"`
OnlyUntagged bool `json:"only_untagged,omitempty" yaml:"only_untagged,omitempty"`
TimeConstraint *GCTimeConstraint `json:"time_constraint,omitempty" yaml:"time_constraint,omitempty"`
Action string `json:"action" yaml:"action"`
}

// GCTimeConstraint appears in type GCPolicy.
type GCTimeConstraint struct {
FieldName string `json:"on"`
OldestCount uint64 `json:"oldest,omitempty"`
NewestCount uint64 `json:"newest,omitempty"`
MinAge Duration `json:"older_than,omitempty"`
MaxAge Duration `json:"newer_than,omitempty"`
FieldName string `json:"on" yaml:"on"`
OldestCount uint64 `json:"oldest,omitempty" yaml:"oldest,omitempty"`
NewestCount uint64 `json:"newest,omitempty" yaml:"newest,omitempty"`
MinAge Duration `json:"older_than,omitempty" yaml:"older_than,omitempty"`
MaxAge Duration `json:"newer_than,omitempty" yaml:"newer_than,omitempty"`
}

// MatchesRepository evaluates the repository regexes in this policy.
Expand Down
2 changes: 2 additions & 0 deletions internal/keppel/models.go
Expand Up @@ -52,6 +52,8 @@ type Account struct {
RequiredLabels string `db:"required_labels"`
// InMaintenance indicates whether the account is in maintenance mode (as defined in the API spec).
InMaintenance bool `db:"in_maintenance"`
// IsManaged indicates if the account was created by AccountManagementDriver
IsManaged bool `db:"is_managed"`

// MetadataJSON contains a JSON string of a map[string]string, or the empty string.
MetadataJSON string `db:"metadata_json"`
Expand Down
8 changes: 4 additions & 4 deletions internal/keppel/rbac_policy.go
Expand Up @@ -31,10 +31,10 @@ import (
// RBACPolicy is a policy granting user-defined access to repos in an account.
// It is stored in serialized form in the RBACPoliciesJSON field of type Account.
type RBACPolicy struct {
CidrPattern string `json:"match_cidr,omitempty"`
RepositoryPattern regexpext.BoundedRegexp `json:"match_repository,omitempty"`
UserNamePattern regexpext.BoundedRegexp `json:"match_username,omitempty"`
Permissions []RBACPermission `json:"permissions"`
CidrPattern string `json:"match_cidr,omitempty" yaml:"match_cidr,omitempty"`
RepositoryPattern regexpext.BoundedRegexp `json:"match_repository,omitempty" yaml:"match_repository,omitempty"`
UserNamePattern regexpext.BoundedRegexp `json:"match_username,omitempty" yaml:"match_username,omitempty"`
Permissions []RBACPermission `json:"permissions" yaml:"permissions"`
}

// RBACPermission enumerates permissions that can be granted by an RBAC policy.
Expand Down
16 changes: 8 additions & 8 deletions internal/keppel/replication.go
Expand Up @@ -27,18 +27,18 @@ import (

// ReplicationPolicy represents a replication policy in the API.
type ReplicationPolicy struct {
Strategy string `json:"strategy"`
//only for `on_first_use`
UpstreamPeerHostName string `json:"upstream_peer_hostname"`
//only for `from_external_on_first_use`
ExternalPeer ReplicationExternalPeerSpec `json:"external_peer"`
Strategy string `json:"strategy" yaml:"strategy"`
// only for `on_first_use`
UpstreamPeerHostName string `json:"upstream_peer_hostname" yaml:"upstream_peer_hostname"`
// only for `from_external_on_first_use`
ExternalPeer ReplicationExternalPeerSpec `json:"external_peer" yaml:"external_peer"`
}

// ReplicationExternalPeerSpec appears in type ReplicationPolicy.
type ReplicationExternalPeerSpec struct {
URL string `json:"url"`
UserName string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
URL string `json:"url" yaml:"url"`
UserName string `json:"username,omitempty" yaml:"username,omitempty"`
Password string `json:"password,omitempty" yaml:"password,omitempty"`
}

// MarshalJSON implements the json.Marshaler interface.
Expand Down

0 comments on commit cfeabb4

Please sign in to comment.