Skip to content
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