System wide RBAC changes
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
package {{ .Package }}
|
||||
|
||||
{{ template "header-gentext.tpl" }}
|
||||
{{ template "header-definitions.tpl" . }}
|
||||
|
||||
import (
|
||||
{{- range .Imports }}
|
||||
{{ . }}
|
||||
{{- end }}
|
||||
)
|
||||
|
||||
{{- range .Def }}
|
||||
{{- if gt (len .RBAC.Resource.References) 0 }}
|
||||
// {{ export .Component .Resource }}RbacReferences generates RBAC references
|
||||
//
|
||||
// Resources with "envoy: false" are skipped
|
||||
//
|
||||
// This function is auto-generated
|
||||
func {{ export .Component .Resource }}RbacReferences({{- range .RBAC.Resource.References }}{{ unexport .Resource }} string, {{- end }}) (res *Ref, pp []*Ref, err error) {
|
||||
{{- range .RBAC.Resource.References }}
|
||||
{{- if eq .Field "ID" }}
|
||||
if {{ unexport .Resource }} != "*" {
|
||||
res = &Ref{ResourceType: types.{{ export .Resource }}ResourceType, Identifiers: MakeIdentifiers({{ unexport .Resource }})}
|
||||
}
|
||||
{{- else }}
|
||||
if {{ unexport .Resource }} != "*" {
|
||||
pp = append(pp, &Ref{ResourceType: types.{{ export .Resource }}ResourceType, Identifiers: MakeIdentifiers({{ unexport .Resource }})})
|
||||
}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
return
|
||||
}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,71 @@
|
||||
package {{ .Package }}
|
||||
|
||||
{{ template "header-gentext.tpl" }}
|
||||
{{ template "header-definitions.tpl" . }}
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
{{- range .Imports }}
|
||||
{{ . }}
|
||||
{{- end }}
|
||||
)
|
||||
|
||||
|
||||
// Parse generates resource setting logic for each resource
|
||||
//
|
||||
// Resources with "envoy: false" are skipped
|
||||
//
|
||||
// This function is auto-generated
|
||||
func ParseRule(res string) (string, *Ref, []*Ref, error) {
|
||||
if res == "" {
|
||||
return "", nil, nil, fmt.Errorf("empty resource")
|
||||
}
|
||||
|
||||
sp := "/"
|
||||
|
||||
res = strings.TrimSpace(res)
|
||||
res = strings.TrimRight(res, sp)
|
||||
rr := strings.Split(res, sp)
|
||||
|
||||
// only service defined (corteza::system, corteza::compose, ...)
|
||||
if len(rr) == 1 {
|
||||
return res, nil, nil, nil
|
||||
}
|
||||
|
||||
// full thing
|
||||
resourceType, path := rr[0], rr[1:]
|
||||
|
||||
for p := 1; p < len(path); p++ {
|
||||
if path[p] != "*" && path[p-1] == "*" {
|
||||
return "", nil, nil, fmt.Errorf("invalid path wildcard combination for '%s'", res)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// make the resource provide the slice of parent resources we should nest under
|
||||
switch resourceType {
|
||||
{{- range .Def }}
|
||||
case {{ unexport .Component "types" }}.{{ export .Resource }}ResourceType:
|
||||
if len(path) != {{ len .RBAC.Resource.References }} {
|
||||
return "", nil, nil, fmt.Errorf("expecting {{ len .RBAC.Resource.References }} reference components in path, got %d", len(path))
|
||||
}
|
||||
{{- if gt (len .RBAC.Resource.References) 0 }}
|
||||
ref, pp, err := {{ export .Component .Resource }}RbacReferences(
|
||||
{{- range $i, $r := .RBAC.Resource.References }}
|
||||
// {{ unexport $r.Resource }}
|
||||
path[{{ $i }}],
|
||||
{{ end }}
|
||||
)
|
||||
return {{ unexport .Component "types" }}.{{ export .Resource }}ResourceType, ref, pp, err
|
||||
{{ else }}
|
||||
|
||||
// Component resource, no path
|
||||
return {{ unexport .Component "types" }}.{{ export .Resource }}ResourceType, nil, nil, nil
|
||||
{{- end }}
|
||||
{{- end}}
|
||||
}
|
||||
|
||||
// return unhandled resource as-is
|
||||
return resourceType, nil, nil, nil
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// Definitions file that controls how this file is generated:
|
||||
{{- range .Def }}
|
||||
// - {{ .Source }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,5 @@
|
||||
// This file is auto-generated.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
//
|
||||
@@ -0,0 +1,256 @@
|
||||
package {{ .Package }}
|
||||
|
||||
{{ template "header-gentext.tpl" }}
|
||||
{{ template "header-definitions.tpl" . }}
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/spf13/cast"
|
||||
"strings"
|
||||
"context"
|
||||
"github.com/cortezaproject/corteza-server/pkg/rbac"
|
||||
"github.com/cortezaproject/corteza-server/pkg/actionlog"
|
||||
{{- range .Imports }}
|
||||
{{ . }}
|
||||
{{- end }}
|
||||
)
|
||||
|
||||
|
||||
type (
|
||||
accessControl struct {
|
||||
actionlog actionlog.Recorder
|
||||
|
||||
rbac interface {
|
||||
Can(rbac.Session, string, rbac.Resource) bool
|
||||
Grant(context.Context, ...*rbac.Rule) error
|
||||
FindRulesByRoleID(roleID uint64) (rr rbac.RuleSet)
|
||||
}
|
||||
|
||||
}
|
||||
)
|
||||
|
||||
func AccessControl() *accessControl {
|
||||
return &accessControl{
|
||||
rbac: rbac.Global(),
|
||||
actionlog: DefaultActionlog,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func (svc accessControl) can(ctx context.Context, op string, res rbac.Resource) bool {
|
||||
return svc.rbac.Can(rbac.ContextToSession(ctx), op, res)
|
||||
}
|
||||
|
||||
// Effective returns a list of effective permissions for all given resource
|
||||
func (svc accessControl) Effective(ctx context.Context, rr ... rbac.Resource) (ee rbac.EffectiveSet) {
|
||||
for _, res := range rr {
|
||||
r := res.RbacResource()
|
||||
for op := range rbacResourceOperations(r) {
|
||||
ee.Push(r, op, svc.can(ctx, op, res))
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (svc accessControl) List() (out []map[string]string) {
|
||||
def := []map[string]string{
|
||||
{{- range .Def }}
|
||||
{{- $Resource := .Resource }}
|
||||
{{- $RbacResource := .RBAC.Resource }}
|
||||
{{- range .RBAC.Operations }}
|
||||
{
|
||||
"type": types.{{ coalesce $Resource }}ResourceType,
|
||||
"any": types.{{ coalesce $Resource }}RbacResource({{ range $RbacResource.References }}0,{{ end }}),
|
||||
"op": {{ printf "%q" .Operation }},
|
||||
},
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
}
|
||||
|
||||
func(svc interface{}) {
|
||||
if svc, is := svc.(interface{}).(interface{ list() []map[string]string }); is {
|
||||
def = append(def, svc.list()...)
|
||||
}
|
||||
}(svc)
|
||||
|
||||
return def
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Grant applies one or more RBAC rules
|
||||
//
|
||||
// This function is auto-generated
|
||||
func (svc accessControl) Grant(ctx context.Context, rr ...*rbac.Rule) error {
|
||||
if !svc.CanGrant(ctx) {
|
||||
// @todo should be altered to check grant permissions PER resource
|
||||
return AccessControlErrNotAllowedToSetPermissions()
|
||||
}
|
||||
|
||||
for _, r := range rr {
|
||||
err := rbacResourceValidator(r.Resource, r.Operation)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if err := svc.rbac.Grant(ctx, rr...); err != nil {
|
||||
return AccessControlErrGeneric().Wrap(err)
|
||||
}
|
||||
|
||||
svc.logGrants(ctx, rr)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// This function is auto-generated
|
||||
func (svc accessControl) logGrants(ctx context.Context, rr []*rbac.Rule) {
|
||||
if svc.actionlog == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, r := range rr {
|
||||
g := AccessControlActionGrant(&accessControlActionProps{r})
|
||||
g.log = r.String()
|
||||
g.resource = r.Resource
|
||||
|
||||
svc.actionlog.Record(ctx, g.ToAction())
|
||||
}
|
||||
}
|
||||
|
||||
// FindRulesByRoleID find all rules for a specific role
|
||||
//
|
||||
// This function is auto-generated
|
||||
func (svc accessControl) FindRulesByRoleID(ctx context.Context, roleID uint64) (rbac.RuleSet, error) {
|
||||
if !svc.CanGrant(ctx) {
|
||||
return nil, AccessControlErrNotAllowedToSetPermissions()
|
||||
}
|
||||
|
||||
return svc.rbac.FindRulesByRoleID(roleID), nil
|
||||
}
|
||||
|
||||
|
||||
{{- range .Def }}
|
||||
{{ $GoType := printf "types.%s" (.Resource) }}
|
||||
|
||||
{{ if .IsComponentResource }}
|
||||
|
||||
{{- range .RBAC.Operations }}
|
||||
// {{ export .CanFnName }} checks if current user can {{ lower .Description }}
|
||||
//
|
||||
// This function is auto-generated
|
||||
func (svc accessControl) {{ export .CanFnName }}(ctx context.Context) bool {
|
||||
return svc.can(ctx, {{ printf "%q" .Operation }}, &types.Component{})
|
||||
}
|
||||
{{- end }}
|
||||
|
||||
{{ else }}
|
||||
{{- range .RBAC.Operations }}
|
||||
// {{ export .CanFnName }} checks if current user can {{ lower .Description }}
|
||||
//
|
||||
// This function is auto-generated
|
||||
func (svc accessControl) {{ export .CanFnName }}(ctx context.Context, r * {{ $GoType }}) bool {
|
||||
return svc.can(ctx, {{ printf "%q" .Operation }}, r)
|
||||
}
|
||||
{{- end }}
|
||||
|
||||
{{ end }}
|
||||
{{- end }}
|
||||
|
||||
|
||||
// rbacResourceValidator validates known component's resource by routing it to the appropriate validator
|
||||
//
|
||||
// This function is auto-generated
|
||||
func rbacResourceValidator(r string, oo ...string) error {
|
||||
switch rbac.ResourceType(r) {
|
||||
{{- range .Def }}
|
||||
case types.{{ coalesce .Resource }}ResourceType:
|
||||
return rbac{{ .Resource }}ResourceValidator(r, oo...)
|
||||
{{- end }}
|
||||
}
|
||||
|
||||
return fmt.Errorf("unknown resource type '%q'", r)
|
||||
}
|
||||
|
||||
// rbacResourceOperations returns defined operations for a requested resource
|
||||
//
|
||||
// This function is auto-generated
|
||||
func rbacResourceOperations(r string) map[string]bool {
|
||||
switch rbac.ResourceType(r) {
|
||||
{{- range .Def }}
|
||||
case types.{{ coalesce .Resource }}ResourceType:
|
||||
return map[string]bool{
|
||||
{{- range .RBAC.Operations }}
|
||||
{{ printf "%q" .Operation }}: true,
|
||||
{{- end }}
|
||||
}
|
||||
{{- end }}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
{{- range .Def }}
|
||||
|
||||
{{ $Resource := .Resource }}
|
||||
{{ $GoType := printf "types.%s" (.Resource) }}
|
||||
|
||||
// rbac{{ .Resource }}ResourceValidator checks validity of rbac resource and operations
|
||||
//
|
||||
// Can be called without operations to check for validity of resource string only
|
||||
//
|
||||
// This function is auto-generated
|
||||
func rbac{{ .Resource }}ResourceValidator(r string, oo ...string) error {
|
||||
defOps := rbacResourceOperations(r)
|
||||
for _, o := range oo {
|
||||
if !defOps[o] {
|
||||
return fmt.Errorf("invalid operation '%s' for {{ .Component }}{{ if not .IsComponentResource }} {{ .Resource }}{{end }} resource", o)
|
||||
}
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(r, {{ $GoType }}ResourceType) {
|
||||
// expecting resource to always include path
|
||||
return fmt.Errorf("invalid resource type")
|
||||
}
|
||||
|
||||
|
||||
{{ if .RBAC.Resource.References }}
|
||||
const sep = "/"
|
||||
var (
|
||||
specIdUsed = true
|
||||
|
||||
pp = strings.Split(strings.Trim(r[len({{ $GoType }}ResourceType):], sep), sep)
|
||||
prc = []string{
|
||||
{{- range .RBAC.Resource.References }}
|
||||
{{ printf "%q" .Field }},
|
||||
{{- end }}
|
||||
}
|
||||
)
|
||||
|
||||
if len(pp) != len(prc) {
|
||||
return fmt.Errorf("invalid resource path structure")
|
||||
}
|
||||
|
||||
for i, p := range pp {
|
||||
if p == "*" {
|
||||
if !specIdUsed {
|
||||
return fmt.Errorf("invalid resource path wildcard level (%d) for {{ .Resource }}", i)
|
||||
}
|
||||
|
||||
specIdUsed = false
|
||||
continue
|
||||
}
|
||||
|
||||
specIdUsed = true
|
||||
if _, err := cast.ToUint64E(p); err != nil {
|
||||
return fmt.Errorf("invalid reference for %s: '%s'", prc[i], p)
|
||||
}
|
||||
}
|
||||
{{- end }}
|
||||
|
||||
return nil
|
||||
}
|
||||
{{- end }}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
package {{ .Package }}
|
||||
|
||||
{{ template "header-gentext.tpl" }}
|
||||
{{ template "header-definitions.tpl" . }}
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type (
|
||||
// Component struct serves as a virtual resource type for the {{ .Component }} component
|
||||
//
|
||||
// This struct is auto-generated
|
||||
Component struct {}
|
||||
)
|
||||
|
||||
const (
|
||||
{{- range .Def }}
|
||||
{{ coalesce .Resource "Component" }}ResourceType = "{{ .RBAC.ResourceType }}"
|
||||
{{- end }}
|
||||
)
|
||||
|
||||
|
||||
{{- range .Def }}
|
||||
{{ $Resource := .Resource }}
|
||||
{{ $GoType := printf "types.%s" .Resource }}
|
||||
|
||||
|
||||
// RbacResource returns string representation of RBAC resource for {{ .Resource }} by calling {{ .Resource }}RbacResource fn
|
||||
//
|
||||
// RBAC resource is in the {{ .RBAC.ResourceType }}/... format
|
||||
//
|
||||
// This function is auto-generated
|
||||
func (r {{ .Resource }}) RbacResource() string {
|
||||
return {{ .Resource }}RbacResource({{ if .RBAC.Resource }}{{ range .RBAC.Resource.References }}r.{{ export .Field }},{{ end }}{{ end }})
|
||||
}
|
||||
|
||||
// {{ .Resource }}RbacResource returns string representation of RBAC resource for {{ .Resource }}
|
||||
//
|
||||
// RBAC resource is in the {{ .RBAC.ResourceType }}/{{- if .RBAC.Resource.References }}...{{ end }} format
|
||||
//
|
||||
// This function is auto-generated
|
||||
func {{ .Resource }}RbacResource({{ if .RBAC.Resource }}{{ range .RBAC.Resource.References }}{{ unexport .Field }} uint64,{{ end }}{{ end }}) string {
|
||||
{{- if .RBAC.Resource.References }}
|
||||
cpts := []interface{{"{}"}}{{"{"}}{{ .Resource }}ResourceType{{"}"}}
|
||||
{{- range .RBAC.Resource.References }}
|
||||
if {{ unexport .Field }} != 0 {
|
||||
cpts = append(cpts, strconv.FormatUint({{ unexport .Field }}, 10))
|
||||
} else {
|
||||
cpts = append(cpts, "*")
|
||||
}
|
||||
|
||||
{{ end }}
|
||||
return fmt.Sprintf({{ .Resource }}RbacResourceTpl(), cpts...)
|
||||
{{- else }}
|
||||
return {{ .Resource }}ResourceType + "/"
|
||||
{{- end }}
|
||||
|
||||
}
|
||||
|
||||
// @todo template
|
||||
func {{ .Resource }}RbacResourceTpl() string {
|
||||
{{- if .RBAC.Resource.References }}
|
||||
return "%s
|
||||
{{- range .RBAC.Resource.References }}/%s{{- end }}"
|
||||
|
||||
{{- else }}
|
||||
return "%s"
|
||||
{{- end }}
|
||||
}
|
||||
|
||||
{{ if .RBAC.Resource.Attributes }}
|
||||
// RbacAttributes returns resource attributes used for generating list of contextual roles
|
||||
//
|
||||
// This function is auto-generated
|
||||
func (r {{ .Resource }}) RbacAttributes() map[string]interface{} {
|
||||
return {{ unexport .Resource }}RbacAttributes(r)
|
||||
}
|
||||
|
||||
{{ if .RBAC.Resource.Attributes.Fields }}
|
||||
// {{ .Resource }}RbacResource returns string representation of RBAC resource for {{ .Resource }}
|
||||
//
|
||||
// RBAC resource is in the {{ .RBAC.ResourceType }}/... format
|
||||
//
|
||||
// This function is auto-generated
|
||||
func {{ unexport .Resource }}RbacAttributes(r {{ .Resource }}) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
{{- range .RBAC.Resource.Attributes.Fields }}
|
||||
{{ printf "%q" . }}: r.{{ export . }},
|
||||
{{- end }}
|
||||
}
|
||||
}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
|
||||
{{- end }}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package def
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/pkg/codegen-v3/internal/tpl"
|
||||
"github.com/cortezaproject/corteza-server/pkg/y7s"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type (
|
||||
Document struct {
|
||||
Skip bool `yaml:"(skip)"`
|
||||
Imports []string
|
||||
Component string
|
||||
IsComponentResource bool `yaml:"-"`
|
||||
Resource string
|
||||
Source string
|
||||
RBAC *rbac
|
||||
Envoy bool `yaml:"envoy"`
|
||||
}
|
||||
)
|
||||
|
||||
func (set *rbacOperations) UnmarshalYAML(n *yaml.Node) error {
|
||||
return y7s.Each(n, func(k *yaml.Node, v *yaml.Node) (err error) {
|
||||
def := rbacOperation{}
|
||||
if k != nil {
|
||||
def.Operation = k.Value
|
||||
}
|
||||
|
||||
*set = append(*set, &def)
|
||||
return v.Decode(&def)
|
||||
})
|
||||
}
|
||||
|
||||
// Preproc preprocesses the document and sets defaults
|
||||
func (doc *Document) Proc(filename string) error {
|
||||
doc.Source = filename
|
||||
|
||||
// filename parts
|
||||
fp := strings.Split(filename, ".")
|
||||
// trim extension
|
||||
fp = fp[:len(fp)-1]
|
||||
if len(fp) > 0 && doc.Component == "" {
|
||||
// set component from the 1st part
|
||||
// component is system, compose, ...
|
||||
doc.Component = fp[0]
|
||||
}
|
||||
|
||||
if len(fp) > 1 && doc.Resource == "" {
|
||||
// if there are more parts, set resource
|
||||
// resource is user, module, record, workflow, ...
|
||||
doc.Resource = fp[1]
|
||||
}
|
||||
|
||||
if strings.ToLower(doc.Resource) == "component" {
|
||||
return fmt.Errorf("can not use 'component' as a resource name, this is done automatically")
|
||||
} else if doc.Resource == "" {
|
||||
doc.Resource = "component"
|
||||
doc.IsComponentResource = true
|
||||
}
|
||||
|
||||
doc.Imports = normalizeImport(doc.Imports...)
|
||||
|
||||
if err := doc.RBAC.proc(doc.Component, doc.Resource); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
doc.Resource = tpl.Export(doc.Resource)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeImport(ii ...string) []string {
|
||||
for i := range ii {
|
||||
if strings.Contains(ii[i], " ") {
|
||||
p := strings.SplitN(ii[i], " ", 2)
|
||||
ii[i] = fmt.Sprintf(`%s "%s"`, p[0], strings.Trim(p[1], `"`))
|
||||
} else {
|
||||
ii[i] = fmt.Sprintf(`"%s"`, strings.Trim(ii[i], `"'`+"`"))
|
||||
}
|
||||
}
|
||||
|
||||
return ii
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package def
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/pkg/codegen-v3/internal/tpl"
|
||||
"github.com/cortezaproject/corteza-server/pkg/y7s"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type (
|
||||
rbac struct {
|
||||
// fully qualified resource name
|
||||
ResourceType string `yaml:"resourceType"`
|
||||
Resource *rbacResource
|
||||
Operations rbacOperations
|
||||
}
|
||||
|
||||
rbacResource struct {
|
||||
References []*rbacResourceRef
|
||||
Attributes *rbacAttributes
|
||||
}
|
||||
|
||||
rbacResourceRef struct {
|
||||
Field string
|
||||
ResourceType string
|
||||
Resource string
|
||||
Component string
|
||||
|
||||
custom bool
|
||||
}
|
||||
|
||||
rbacOperations []*rbacOperation
|
||||
|
||||
rbacOperation struct {
|
||||
Operation string
|
||||
CanFnName string `yaml:"canFnName"`
|
||||
Description string
|
||||
}
|
||||
|
||||
rbacAttributes struct {
|
||||
Fields []string `yaml:"-"`
|
||||
}
|
||||
)
|
||||
|
||||
func (r *rbac) proc(component, resource string) error {
|
||||
const (
|
||||
defaultNS = "corteza"
|
||||
nsDelimiter = "::"
|
||||
)
|
||||
|
||||
if r.ResourceType == "" {
|
||||
if strings.ToLower(resource) == "component" {
|
||||
r.ResourceType = component
|
||||
} else {
|
||||
r.ResourceType = fmt.Sprintf("%s:%s", component, resource)
|
||||
}
|
||||
|
||||
r.ResourceType = defaultNS + nsDelimiter + r.ResourceType
|
||||
}
|
||||
|
||||
if !strings.Contains(r.ResourceType, nsDelimiter) {
|
||||
return fmt.Errorf("no namespace prefix found (e.g.: 'corteza::') in resource type")
|
||||
}
|
||||
|
||||
for _, op := range r.Operations {
|
||||
// Generate all check name
|
||||
if op.CanFnName == "" {
|
||||
op.CanFnName = RbacOperationCanFnName(resource, op.Operation)
|
||||
}
|
||||
}
|
||||
|
||||
if r.Resource == nil {
|
||||
r.Resource = &rbacResource{References: []*rbacResourceRef{{Field: "ID"}}}
|
||||
}
|
||||
|
||||
// check types of each referenced component
|
||||
// and prefix non-custom components with corteza::<component>
|
||||
// and self-references (field==ID) with own resource type
|
||||
for _, rc := range r.Resource.References {
|
||||
if !rc.custom {
|
||||
if rc.Field == "ID" {
|
||||
rc.ResourceType = r.ResourceType
|
||||
rc.Component = component
|
||||
rc.Resource = resource
|
||||
} else {
|
||||
rc.ResourceType = defaultNS + nsDelimiter + fmt.Sprintf("%s:%s", component, rc.Field)
|
||||
rc.Component = component
|
||||
rc.Resource = rc.Field
|
||||
rc.Field = rc.Field + "ID"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (op *rbacOperation) UnmarshalYAML(n *yaml.Node) error {
|
||||
if y7s.IsKind(n, yaml.ScalarNode) {
|
||||
// @todo handle disabled operations
|
||||
// the idea is that when service operations are defined we implicitly define
|
||||
// RBAC operations. Here, we'll be able to remove implicitly defined operation
|
||||
return nil
|
||||
}
|
||||
|
||||
type auxType rbacOperation
|
||||
var aux = (*auxType)(op)
|
||||
return n.Decode(aux)
|
||||
}
|
||||
|
||||
func (op *rbacResourceRef) UnmarshalYAML(n *yaml.Node) error {
|
||||
if y7s.IsKind(n, yaml.ScalarNode) {
|
||||
op.Field = n.Value
|
||||
if n.Value != "ID" {
|
||||
op.ResourceType = n.Value
|
||||
// @todo expand resource & component
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type auxType rbacResourceRef
|
||||
var aux = (*auxType)(op)
|
||||
aux.custom = true
|
||||
return n.Decode(aux)
|
||||
}
|
||||
|
||||
func (a *rbacAttributes) UnmarshalYAML(n *yaml.Node) error {
|
||||
if y7s.IsKind(n, yaml.ScalarNode) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// if not scalar, assume we will get list of fields
|
||||
a.Fields = make([]string, 0)
|
||||
return n.Decode(&a.Fields)
|
||||
}
|
||||
|
||||
func RbacOperationCanFnName(res, op string) string {
|
||||
// when check function name is not explicitly defined we try
|
||||
// to use resource and operation name and generate easy-to-read name
|
||||
//
|
||||
// <res> + <op> => Can<Op><Res>
|
||||
// <res> + <op:foo.bar.verb> => Can<Verb><Foo><Bar>On<Res>
|
||||
|
||||
if strings.ToLower(res) == "component" {
|
||||
res = ""
|
||||
}
|
||||
|
||||
if strings.Contains(op, ".") {
|
||||
parts := strings.Split(op, ".")
|
||||
l := len(parts)
|
||||
|
||||
parts = append(parts[l-1:], parts[:l-1]...)
|
||||
|
||||
if res != "" {
|
||||
// Only append "on" if there is resource
|
||||
parts = append(parts, "on")
|
||||
}
|
||||
|
||||
op = tpl.Export(parts...)
|
||||
}
|
||||
|
||||
return tpl.Export("can", op, res)
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package gen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"text/template"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/pkg/codegen-v3/internal/def"
|
||||
"github.com/cortezaproject/corteza-server/pkg/codegen-v3/internal/tpl"
|
||||
"github.com/cortezaproject/corteza-server/pkg/slice"
|
||||
)
|
||||
|
||||
func Envoy(t *template.Template, dd []*def.Document) error {
|
||||
return List{
|
||||
"resource rbac parse": envoyResourceRbacUnmarshal,
|
||||
"resource rbac references": envoyResourceRbacReferences,
|
||||
}.Generate(t, dd)
|
||||
}
|
||||
|
||||
// EnvoyResourceRbacUnmarshal envoy rbac unmarshal
|
||||
// <service>/service/rbac.gen.go
|
||||
//
|
||||
// Contains all RBAC related definitions
|
||||
func envoyResourceRbacUnmarshal(t *template.Template, dd []*def.Document) (err error) {
|
||||
const (
|
||||
templateName = "envoy/resource-rbac_rules_parse.go.tpl"
|
||||
outputPathTpl = "pkg/envoy/resource/rbac_rules_parse.gen.go"
|
||||
)
|
||||
|
||||
dd = filter(dd, func(d *def.Document) bool { return d.Envoy })
|
||||
|
||||
// build list of component type imports
|
||||
ctImports := make([]string, 0)
|
||||
for _, d := range dd {
|
||||
imp := d.Component + "Types " + cImport(d.Component, "types")
|
||||
if !slice.HasString(ctImports, imp) {
|
||||
ctImports = append(ctImports, imp)
|
||||
}
|
||||
}
|
||||
|
||||
w := tpl.Wrap{
|
||||
Package: "resource",
|
||||
Def: dd,
|
||||
Imports: append(collectImports(dd...), ctImports...),
|
||||
}
|
||||
|
||||
err = tpl.GoTemplate(outputPathTpl, t.Lookup(templateName), w)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// EnvoyResourceRbacReferences generates one rbac definition file per component
|
||||
// <service>/service/rbac.gen.go
|
||||
//
|
||||
// Contains all RBAC related definitions
|
||||
func envoyResourceRbacReferences(t *template.Template, dd []*def.Document) (err error) {
|
||||
const (
|
||||
templateName = "envoy/resource-rbac_references.go.tpl"
|
||||
outputPathTpl = "pkg/envoy/resource/rbac_references_%s.gen.go"
|
||||
)
|
||||
|
||||
dd = filter(dd, func(d *def.Document) bool { return d.Envoy })
|
||||
|
||||
for component, perComponent := range partByComponent(dd) {
|
||||
|
||||
w := tpl.Wrap{
|
||||
Package: "resource",
|
||||
Component: component,
|
||||
Def: perComponent,
|
||||
Imports: append(collectImports(perComponent...), cImport(component, "types")),
|
||||
}
|
||||
|
||||
err = tpl.GoTemplate(fmt.Sprintf(outputPathTpl, component), t.Lookup(templateName), w)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
Generated
+67
@@ -0,0 +1,67 @@
|
||||
package gen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"text/template"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/pkg/codegen-v3/internal/def"
|
||||
)
|
||||
|
||||
type (
|
||||
List map[string]func(*template.Template, []*def.Document) error
|
||||
)
|
||||
|
||||
func (gg List) Generate(tpls *template.Template, dd []*def.Document) (err error) {
|
||||
for l, g := range gg {
|
||||
if err = g(tpls, dd); err != nil {
|
||||
return fmt.Errorf("codegen for %s failed: %w", l, err)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func filter(dd []*def.Document, check func(*def.Document) bool) []*def.Document {
|
||||
aux := make([]*def.Document, 0, len(dd))
|
||||
for _, d := range dd {
|
||||
if !check(d) {
|
||||
continue
|
||||
}
|
||||
|
||||
aux = append(aux, d)
|
||||
}
|
||||
return aux
|
||||
}
|
||||
|
||||
func partByComponent(dd []*def.Document) map[string][]*def.Document {
|
||||
var (
|
||||
parted = make(map[string][]*def.Document)
|
||||
)
|
||||
|
||||
for _, d := range dd {
|
||||
parted[d.Component] = append(parted[d.Component], d)
|
||||
}
|
||||
|
||||
return parted
|
||||
}
|
||||
|
||||
func collectImports(dd ...*def.Document) []string {
|
||||
mm := make(map[string]bool)
|
||||
for _, d := range dd {
|
||||
for _, i := range d.Imports {
|
||||
mm[i] = true
|
||||
}
|
||||
}
|
||||
|
||||
ii := make([]string, 0, len(mm))
|
||||
for i := range mm {
|
||||
ii = append(ii, i)
|
||||
}
|
||||
|
||||
return ii
|
||||
}
|
||||
|
||||
// component import
|
||||
func cImport(c, s string) string {
|
||||
return fmt.Sprintf(`"github.com/cortezaproject/corteza-server/%s/%s"`, c, s)
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package gen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"text/template"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/pkg/codegen-v3/internal/def"
|
||||
"github.com/cortezaproject/corteza-server/pkg/codegen-v3/internal/tpl"
|
||||
)
|
||||
|
||||
func RBAC(t *template.Template, dd []*def.Document) error {
|
||||
return List{
|
||||
"type": rbacTypes,
|
||||
"service access control": rbacAccessControlService,
|
||||
}.Generate(t, dd)
|
||||
}
|
||||
|
||||
// RbacTypes generates rbac definitions (one per component)
|
||||
// <service>/service/rbac.gen.go
|
||||
//
|
||||
// Contains all RBAC related definitions
|
||||
func rbacTypes(t *template.Template, dd []*def.Document) (err error) {
|
||||
const (
|
||||
templateName = "rbac/types.go.tpl"
|
||||
outputPathTpl = "%s/types/rbac.gen.go"
|
||||
)
|
||||
|
||||
for component, perComponent := range partByComponent(dd) {
|
||||
w := tpl.Wrap{
|
||||
Package: "types",
|
||||
Component: component,
|
||||
Def: perComponent,
|
||||
}
|
||||
|
||||
err = tpl.GoTemplate(fmt.Sprintf(outputPathTpl, component), t.Lookup(templateName), w)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// RbacAccessControlService generates access control functions (one file per component)
|
||||
// <service>/service/rbac.gen.go
|
||||
//
|
||||
// Contains all RBAC related definitions
|
||||
func rbacAccessControlService(t *template.Template, dd []*def.Document) (err error) {
|
||||
const (
|
||||
templateName = "rbac/access_control.go.tpl"
|
||||
outputPathTpl = "%s/service/access_control.gen.go"
|
||||
)
|
||||
|
||||
for component, perComponent := range partByComponent(dd) {
|
||||
w := tpl.Wrap{
|
||||
Package: "service",
|
||||
Component: component,
|
||||
Def: perComponent,
|
||||
Imports: append(collectImports(perComponent...), cImport(component, "types")),
|
||||
}
|
||||
|
||||
err = tpl.GoTemplate(fmt.Sprintf(outputPathTpl, component), t.Lookup(templateName), w)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package tpl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/format"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/Masterminds/sprig"
|
||||
)
|
||||
|
||||
type (
|
||||
Wrap struct {
|
||||
Package string
|
||||
|
||||
// will be set when grouping definitions by component
|
||||
Component string
|
||||
|
||||
Imports []string
|
||||
Def interface{}
|
||||
}
|
||||
)
|
||||
|
||||
var nonIdentChars = regexp.MustCompile(`[\s\\/\-.]+`)
|
||||
|
||||
func Export(pp ...string) (out string) {
|
||||
for _, p := range pp {
|
||||
if len(p) > 1 {
|
||||
p = strings.ToUpper(p[:1]) + p[1:]
|
||||
}
|
||||
|
||||
if ss := nonIdentChars.Split(p, -1); len(ss) > 1 {
|
||||
p = Export(ss...)
|
||||
}
|
||||
|
||||
out = out + p
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func Unexport(pp ...string) (out string) {
|
||||
out = Export(pp...)
|
||||
if len(out) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if out == "ID" {
|
||||
return "id"
|
||||
}
|
||||
|
||||
return strings.ToLower(out[:1]) + out[1:]
|
||||
}
|
||||
|
||||
func BaseTemplate() *template.Template {
|
||||
return template.New("").
|
||||
Funcs(sprig.TxtFuncMap()).
|
||||
Funcs(map[string]interface{}{
|
||||
"export": Export,
|
||||
"unexport": Unexport,
|
||||
})
|
||||
}
|
||||
|
||||
func LoadTemplates(rTpl *template.Template, rootDir string) (*template.Template, error) {
|
||||
cleanRoot := filepath.Clean(rootDir)
|
||||
pfx := len(cleanRoot) + 1
|
||||
|
||||
return rTpl, filepath.Walk(cleanRoot, func(path string, info os.FileInfo, err error) error {
|
||||
if info.IsDir() || !strings.HasSuffix(path, ".tpl") || err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
name := path[pfx:]
|
||||
rTpl, err = rTpl.New(name).Parse(string(b))
|
||||
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func GoTemplate(dst string, tpl *template.Template, payload Wrap) (err error) {
|
||||
var output io.WriteCloser
|
||||
buf := bytes.Buffer{}
|
||||
|
||||
if tpl == nil {
|
||||
return fmt.Errorf("could not find template for %s", dst)
|
||||
}
|
||||
|
||||
if err := tpl.Execute(&buf, payload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmtsrc, err := format.Source(buf.Bytes())
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintf(os.Stderr, "%s fmt warn: %v\n", dst, err)
|
||||
|
||||
err = nil
|
||||
fmtsrc = buf.Bytes()
|
||||
}
|
||||
|
||||
if dst == "" || dst == "-" {
|
||||
output = os.Stdout
|
||||
} else {
|
||||
if output, err = os.Create(dst); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer output.Close()
|
||||
}
|
||||
|
||||
if _, err = output.Write(fmtsrc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/pkg/cli"
|
||||
"github.com/cortezaproject/corteza-server/pkg/codegen-v3/internal/def"
|
||||
"github.com/cortezaproject/corteza-server/pkg/codegen-v3/internal/gen"
|
||||
"github.com/cortezaproject/corteza-server/pkg/codegen-v3/internal/tpl"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var _ = spew.Dump
|
||||
|
||||
func main() {
|
||||
dd, err := loadDefinitions(os.Args[1])
|
||||
cli.HandleError(err)
|
||||
|
||||
tpls, err := tpl.LoadTemplates(tpl.BaseTemplate(), "./pkg/codegen-v3/assets/templates/gocode")
|
||||
if err != nil {
|
||||
cli.HandleError(fmt.Errorf("could not load templates: %w", err))
|
||||
}
|
||||
|
||||
cli.HandleError(gen.List{
|
||||
"RBAC": gen.RBAC,
|
||||
"Envoy": gen.Envoy,
|
||||
}.Generate(tpls, dd))
|
||||
}
|
||||
|
||||
func loadDefinition(r io.Reader) (*def.Document, error) {
|
||||
doc := &def.Document{
|
||||
Envoy: true,
|
||||
}
|
||||
|
||||
return doc, yaml.NewDecoder(r).Decode(doc)
|
||||
}
|
||||
|
||||
func loadDefinitions(path string) (dd []*def.Document, err error) {
|
||||
var (
|
||||
fh *os.File
|
||||
doc *def.Document
|
||||
files []string
|
||||
)
|
||||
|
||||
files, err = filepath.Glob(path + "/*.yaml")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not load ddefinitions form path '%s': %w", path, err)
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
fh, err = os.Open(file)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not load definiton file '%s': %w", file, err)
|
||||
}
|
||||
|
||||
doc, err = loadDefinition(fh)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not load definiton from '%s': %w", file, err)
|
||||
}
|
||||
|
||||
if doc.Skip {
|
||||
continue
|
||||
}
|
||||
|
||||
if err = doc.Proc(filepath.Base(file)); err != nil {
|
||||
return nil, fmt.Errorf("failed to preprocess definitions from '%s': %w", file, err)
|
||||
}
|
||||
|
||||
dd = append(dd, doc)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
Reference in New Issue
Block a user