Skip to content

Commit

Permalink
Add AccountManagementDriver
Browse files Browse the repository at this point in the history
  • Loading branch information
SuperSandro2000 committed Mar 14, 2024
1 parent b7e9aea commit 25d3e23
Show file tree
Hide file tree
Showing 6 changed files with 340 additions and 3 deletions.
6 changes: 3 additions & 3 deletions internal/api/keppel/accounts.go
Expand Up @@ -62,11 +62,11 @@ type Account struct {

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

// ReplicationExternalPeerSpec appears in type ReplicationPolicy.
Expand Down
146 changes: 146 additions & 0 deletions internal/drivers/basic/account_management.go
@@ -0,0 +1,146 @@
/******************************************************************************
*
* 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"

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

"gopkg.in/yaml.v2"
)

// AccountManagementDriver is the account management driver "basic".
type AccountManagementDriver struct {
configPath string
config AccountConfig
skipConfigLoading bool // used in tests
}

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 keppelv1.ReplicationPolicy `yaml:"replication_policy"`
SecurityScanPolicies []keppel.SecurityScanPolicy `yaml:"security_scan_policies"`
ValidationPolicy keppelv1.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) {
if !a.skipConfigLoading {
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
}
118 changes: 118 additions & 0 deletions internal/drivers/basic/account_management_test.go
@@ -0,0 +1,118 @@
/******************************************************************************
*
* 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"
"time"

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

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

func TestAccountManagementDriver(t *testing.T) {
driver := AccountManagementDriver{
skipConfigLoading: true,
}

driver.config.Accounts = []Accounts{
{
Name: "abcde",
AuthTenantID: "1245",
GCPolicies: []keppel.GCPolicy{
{
RepositoryRx: ".*/database",
NegativeRepositoryRx: "archive/.*",
TimeConstraint: &keppel.GCTimeConstraint{
FieldName: "pushed_at",
MaxAge: keppel.Duration(6 * time.Hour),
},
Action: "protect",
},
{
RepositoryRx: ".*",
OnlyUntagged: true,
Action: "delete",
},
},
RBACPolicies: []keppel.RBACPolicy{
{
RepositoryPattern: "library/.*",
Permissions: []keppel.RBACPermission{keppel.GrantsAnonymousPull},
},
{
RepositoryPattern: "library/alpine",
UserNamePattern: ".*@tenant2",
Permissions: []keppel.RBACPermission{keppel.GrantsPull, keppel.GrantsPush},
},
},
ReplicationPolicy: keppelv1.ReplicationPolicy{
Strategy: "from_external_on_first_use",
ExternalPeer: keppelv1.ReplicationExternalPeerSpec{URL: "registry-tertiary.example.org"},
},
SecurityScanPolicies: []keppel.SecurityScanPolicy{
{
RepositoryRx: ".*",
VulnerabilityIDRx: ".*",
ExceptFixReleased: true,
Action: keppel.SecurityScanPolicyAction{
Ignore: true,
Assessment: "risk accepted: vulnerabilities without an available fix are not actionable",
},
},
},
ValidationPolicy: keppelv1.ValidationPolicy{
RequiredLabels: []string{
"important-label",
"some-label",
},
},
},
}

account := keppel.Account{
IsManaged: true,
Name: "abcde",
AuthTenantID: "1245",
}
newAccount, err := driver.ConfigureAccount(nil, account)
if err != nil {
t.Errorf(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", expectedAccount, newAccount)

listOfAccounts, err := driver.ManagedAccountNames()
if err != nil {
t.Errorf(err.Error())
}
assert.DeepEqual(t, "account", listOfAccounts, []string{"abcde"})
}
63 changes: 63 additions & 0 deletions internal/keppel/account_management_driver.go
@@ -0,0 +1,63 @@
/******************************************************************************
*
* 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 (
"errors"

"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.
//
// Returns ErrSkipAccount if the account is not managed.
// The jobloop will leave the account unchanged.
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().
//
// If names stop appearing in the list, Keppel will delete the
// respective accounts (including all manifests, blobs and repos).
ManagedAccountNames() ([]string, error)
}

// ErrSkipAccount can be returned by AccountManagementDriver.ConfigureAccount.
// See documentation over there for details.
var ErrSkipAccount = errors.New("account is not managed")

// 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
2 changes: 2 additions & 0 deletions internal/keppel/models.go
Expand Up @@ -51,6 +51,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

0 comments on commit 25d3e23

Please sign in to comment.