Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
b7e9aea
commit 25d3e23
Showing
6 changed files
with
340 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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"}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters