Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
.work
_output
__debug_bin
.tool-versions
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ Check the example:
2. Create managed resources for your SQL server flavor:

- **MySQL**: `Database`, `Grant`, `User` (See [the examples](examples/mysql))
- **PostgreSQL**: `Database`, `Grant`, `Extension`, `Role` (See [the examples](examples/postgresql))
- **PostgreSQL**: `Database`, `Schema`, `Grant`, `Extension`, `Role` (See [the examples](examples/postgresql))
- **MSSQL**: `Database`, `Grant`, `User` (See [the examples](examples/mssql))

[crossplane]: https://crossplane.io
Expand Down
230 changes: 221 additions & 9 deletions apis/postgresql/v1alpha1/grant_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,18 @@ package v1alpha1
import (
"context"

"github.com/pkg/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"

xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
"github.com/crossplane/crossplane-runtime/pkg/reference"
"github.com/pkg/errors"
)

const (
errNoPrivileges = "privileges not passed"
errUnknownGrant = "cannot identify grant type based on passed params"
errMemberOfWithPrivileges = "cannot set privileges in the same grant as memberOf"
)

// A GrantSpec defines the desired state of a Grant.
Expand All @@ -43,24 +49,169 @@ type GrantPrivilege string
// +kubebuilder:validation:MinItems:=1
type GrantPrivileges []GrantPrivilege

type GrantType string

// GrantType is the list of the possible grant types represented by a GrantParameters
const (
RoleMember GrantType = "ROLE_MEMBER"
RoleDatabase GrantType = "ROLE_DATABASE"
RoleSchema GrantType = "ROLE_SCHEMA"
RoleTable GrantType = "ROLE_TABLE"
RoleSequence GrantType = "ROLE_SEQUENCE"
RoleRoutine GrantType = "ROLE_ROUTE"
RoleColumn GrantType = "ROLE_COLUMN"
RoleForeignDataWrapper GrantType = "ROLE_FOREIGN_DATA_WRAPPER"
RoleForeignServer GrantType = "ROLE_FOREIGN_SERVER"
)

type marker struct{}
type stringSet struct {
elements map[string]marker
}

func newStringSet() *stringSet {
return &stringSet{
elements: make(map[string]marker),
}
}

func (s *stringSet) add(element string) {
s.elements[element] = marker{}
}

func (s *stringSet) contains(element string) bool {
_, exists := s.elements[element]
return exists
}

func (s *stringSet) containsExactly(elements ...string) bool {
if len(s.elements) != len(elements) {
return false
}
for _, elem := range elements {
if !s.contains(elem) {
return false
}
}
return true
}

func (gp *GrantParameters) filledInFields() *stringSet {
fields := map[string]bool{
"MemberOf": gp.MemberOf != nil,
"Database": gp.Database != nil,
"Schema": gp.Schema != nil,
"Tables": len(gp.Tables) > 0,
"Columns": len(gp.Columns) > 0,
"Sequences": len(gp.Sequences) > 0,
"Routines": len(gp.Routines) > 0,
"ForeignServers": len(gp.ForeignServers) > 0,
"ForeignDataWrappers": len(gp.ForeignDataWrappers) > 0,
}
set := newStringSet()

for key, hasField := range fields {
if hasField {
set.add(key)
}
}
return set
}

var grantTypeFields = map[GrantType][]string{
RoleMember: {"MemberOf"},
RoleDatabase: {"Database"},
RoleSchema: {"Database", "Schema"},
RoleTable: {"Database", "Schema", "Tables"},
RoleColumn: {"Database", "Schema", "Tables", "Columns"},
RoleSequence: {"Database", "Schema", "Sequences"},
RoleRoutine: {"Database", "Schema", "Routines"},
RoleForeignServer: {"Database", "ForeignServers"},
RoleForeignDataWrapper: {"Database", "ForeignDataWrappers"},
}

// IdentifyGrantType return the deduced GrantType from the filled in fields.
func (gp *GrantParameters) IdentifyGrantType() (GrantType, error) {
ff := gp.filledInFields()
pc := len(gp.Privileges)

var gt *GrantType

for k, v := range grantTypeFields {
if ff.containsExactly(v...) {
gt = &k
break
}
}
if gt == nil {
return "", errors.New(errUnknownGrant)
}
if *gt == RoleMember && pc > 0 {
return "", errors.New(errMemberOfWithPrivileges)
}
if *gt != RoleMember && pc < 1 {
return "", errors.New(errNoPrivileges)
}
return *gt, nil
}

// Some privileges are shorthands for multiple privileges. These translations
// happen internally inside postgresql when making grants. When we query the
// privileges back, we need to look for the expanded set.
// https://www.postgresql.org/docs/15/ddl-priv.html
var grantReplacements = map[GrantPrivilege]GrantPrivileges{
"ALL": {"CREATE", "TEMPORARY", "CONNECT"},
"ALL PRIVILEGES": {"CREATE", "TEMPORARY", "CONNECT"},
"TEMP": {"TEMPORARY"},
var grantReplacements = map[GrantType]map[GrantPrivilege]GrantPrivileges{
RoleDatabase: {
"ALL": {"CREATE", "TEMPORARY", "CONNECT"},
"ALL PRIVILEGES": {"CREATE", "TEMPORARY", "CONNECT"},
"TEMP": {"TEMPORARY"},
},
RoleSchema: {
"ALL": {"CREATE", "USAGE"},
"ALL PRIVILEGES": {"CREATE", "USAGE"},
},
RoleTable: {
"ALL": {"SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE", "REFERENCES", "TRIGGER", "MAINTAIN"},
"ALL PRIVILEGES": {"SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE", "REFERENCES", "TRIGGER", "MAINTAIN"},
},
RoleColumn: {
"ALL": {"SELECT", "INSERT", "UPDATE", "REFERENCES"},
"ALL PRIVILEGES": {"SELECT", "INSERT", "UPDATE", "REFERENCES"},
},
RoleSequence: {
"ALL": {"USAGE", "SELECT", "UPDATE"},
"ALL PRIVILEGES": {"USAGE", "SELECT", "UPDATE"},
},
RoleRoutine: {
"ALL": {"EXECUTE"},
"ALL PRIVILEGES": {"EXECUTE"},
},
RoleForeignDataWrapper: {
"ALL": {"USAGE"},
"ALL PRIVILEGES": {"USAGE"},
},
RoleForeignServer: {
"ALL": {"USAGE"},
"ALL PRIVILEGES": {"USAGE"},
},
}

// ExpandPrivileges expands any shorthand privileges to their full equivalents.
func (gp *GrantPrivileges) ExpandPrivileges() GrantPrivileges {
func (gp *GrantParameters) ExpandPrivileges() GrantPrivileges {
gt, err := gp.IdentifyGrantType()
if err != nil {
return gp.Privileges
}
gr, ex := grantReplacements[gt]
if !ex {
return gp.Privileges
}

privilegeSet := make(map[GrantPrivilege]struct{})

// Replace any shorthand privileges with their full equivalents
for _, p := range *gp {
if _, ok := grantReplacements[p]; ok {
for _, rp := range grantReplacements[p] {
for _, p := range gp.Privileges {
if _, ok := gr[p]; ok {
for _, rp := range gr[p] {
privilegeSet[rp] = struct{}{}
}
} else {
Expand Down Expand Up @@ -99,6 +250,15 @@ const (
GrantOptionGrant GrantOption = "GRANT"
)

type Routine struct {
// The name of the routine.
Name string `json:"name,omitempty"`

// The arguments of the routine.
// +optional
Arguments []string `json:"args,omitempty"`
}

// GrantParameters define the desired state of a PostgreSQL grant instance.
type GrantParameters struct {
// Privileges to be granted.
Expand Down Expand Up @@ -141,6 +301,20 @@ type GrantParameters struct {
// +optional
DatabaseSelector *xpv1.Selector `json:"databaseSelector,omitempty"`

// Schema this grant is for.
// +optional
Schema *string `json:"schema,omitempty"`

// SchemaRef references the schema object this grant it for.
// +immutable
// +optional
SchemaRef *xpv1.Reference `json:"schemaRef,omitempty"`

// SchemaSelector selects a reference to a Schema this grant is for.
// +immutable
// +optional
SchemaSelector *xpv1.Selector `json:"schemaSelector,omitempty"`

// MemberOf is the Role that this grant makes Role a member of.
// +optional
MemberOf *string `json:"memberOf,omitempty"`
Expand All @@ -158,6 +332,30 @@ type GrantParameters struct {
// RevokePublicOnDb apply the statement "REVOKE ALL ON DATABASE %s FROM PUBLIC" to make database unreachable from public
// +optional
RevokePublicOnDb *bool `json:"revokePublicOnDb,omitempty" default:"false"`

// The columns upon which to grant the privileges.
// +optional
Columns []string `json:"columns,omitempty"`

// The tables upon which to grant the privileges.
// +optional
Tables []string `json:"tables,omitempty"`

// The sequences upon which to grant the privileges.
// +optional
Sequences []string `json:"sequences,omitempty"`

// The routines upon which to grant the privileges.
// +optional
Routines []Routine `json:"routines,omitempty"`

// The foreign data wrappers upon which to grant the privileges.
// +optional
ForeignDataWrappers []string `json:"foreignDataWrappers,omitempty"`

// The foreign servers upon which to grant the privileges.
// +optional
ForeignServers []string `json:"foreignServers,omitempty"`
}

// A GrantStatus represents the observed state of a Grant.
Expand Down Expand Up @@ -212,6 +410,20 @@ func (mg *Grant) ResolveReferences(ctx context.Context, c client.Reader) error {
mg.Spec.ForProvider.Database = reference.ToPtrValue(rsp.ResolvedValue)
mg.Spec.ForProvider.DatabaseRef = rsp.ResolvedReference

// Resolve spec.forProvider.schema
rsp, err = r.Resolve(ctx, reference.ResolutionRequest{
CurrentValue: reference.FromPtrValue(mg.Spec.ForProvider.Schema),
Reference: mg.Spec.ForProvider.SchemaRef,
Selector: mg.Spec.ForProvider.SchemaSelector,
To: reference.To{Managed: &Schema{}, List: &SchemaList{}},
Extract: reference.ExternalName(),
})
if err != nil {
return errors.Wrap(err, "spec.forProvider.schema")
}
mg.Spec.ForProvider.Schema = reference.ToPtrValue(rsp.ResolvedValue)
mg.Spec.ForProvider.SchemaRef = rsp.ResolvedReference

// Resolve spec.forProvider.role
rsp, err = r.Resolve(ctx, reference.ResolutionRequest{
CurrentValue: reference.FromPtrValue(mg.Spec.ForProvider.Role),
Expand Down
67 changes: 67 additions & 0 deletions apis/postgresql/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading