Merge pull request #78 from RafayLabs/casbin-updates

Authorization updates
This commit is contained in:
abin-rafay
2022-03-25 18:22:06 +05:30
committed by GitHub
11 changed files with 359 additions and 151 deletions
+29
View File
@@ -0,0 +1,29 @@
package dao
import (
"context"
"github.com/google/uuid"
"github.com/uptrace/bun"
)
func GetProjectOrganization(ctx context.Context, db bun.IDB, id uuid.UUID) (string, string, error) {
// Could possibly union them later for some speedup
type projectOrg struct {
Project string
Organization string
}
var r projectOrg
err := db.NewSelect().Table("authsrv_project").
ColumnExpr("authsrv_project.name as project").
ColumnExpr("authsrv_organization.name as organization").
Join(`JOIN authsrv_organization ON authsrv_project.organization_id=authsrv_organization.id`).
Where("authsrv_project.id = ?", id).
Where("authsrv_project.trash = ?", false).
Where("authsrv_organization.trash = ?", false).
Scan(ctx, &r)
if err != nil {
return "", "", err
}
return r.Project, r.Organization, nil
}
+79 -20
View File
@@ -1,7 +1,8 @@
:host = http://localhost:11000
:partner = important-partner
:org = hello
:project = hello
:token = qJeyfD9IthujxqhbaEWOCkX8S3cjwvjk
:partner = partner0
:org = org0
:project = project-uno
:description = this is my first project
:opts.urlScope_user = user/b2e4162c-60df-4fd7-b8fd-8fd3e4d6e533
:opts.urlScope_org = organization/0d95d65f-3ce9-4340-ac05-77f71084d0a6
@@ -24,6 +25,7 @@ opts.urlScope_cluster = cluster/eff85770-1a9e-42bc-824b-d0ff5a0f792c
# Create Project
POST :host/auth/v3/partner/:partner/organization/:organization/project
Content-Type: application/yaml
X-Session-Token: :token
metadata:
partner: :partner
@@ -35,14 +37,17 @@ spec:
# Get all projects
GET :host/auth/v3/partner/:partner/organization/:organization/projects
Content-Type: application/yaml
X-Session-Token: :token
# Get single project
GET :host/auth/v3/partner/:partner/organization/:organization/project/:project
Content-Type: application/yaml
X-Session-Token: :token
# Update project info
PUT :host/auth/v3/partner/:partner/organization/:organization/project/:project
Content-Type: application/yaml
X-Session-Token: :token
metadata:
partner: :partner
@@ -61,6 +66,7 @@ DELETE :host/auth/v3/partner/:partner/organization/:organization/project/:projec
# Publish kubeconfig setting
PUT :host/v2/sentry/kubeconfig/:opts.urlScope_user/setting
Content-Type: application/yaml
X-Session-Token: :token
opts:
organization: :orgid
@@ -78,6 +84,7 @@ GET :host/v2/sentry/kubeconfig/:opts.urlScope_user/setting
# Publish kubeconfig setting
PUT :host/v2/sentry/kubeconfig/:opts.urlScope_org/setting
Content-Type: application/yaml
X-Session-Token: :token
opts:
organization: :orgid
@@ -103,6 +110,7 @@ GET :host/v2/sentry/kubeconfig/user?opts.selector=&opts.account=c6974c2e-ef14-41
# Revoke kubeconfig for user
POST :host/v2/sentry/kubeconfig/revoke
Content-Type: application/yaml
X-Session-Token: :token
opts:
organization: cc02bd83-89d8-4c76-a7dc-06854f93e21d
@@ -111,10 +119,12 @@ opts:
# Kubeconfig get cluster settings
GET :host/v2/sentry/kubectl/:opts.urlScope_cluster/settings?opts.organization=cc02bd83-89d8-4c76-a7dc-06854f93e21d
X-Session-Token: :token
# Publish kubectl settings
PUT :host/v2/sentry/kubectl/:opts.urlScope_cluster/settings
Content-Type: application/yaml
X-Session-Token: :token
opts:
organization: cc02bd83-89d8-4c76-a7dc-06854f93e21d
@@ -129,6 +139,7 @@ GET :host/v2/sentry/authorization/user?userCN=a=b2e4162c-60df-4fd7-b8fd-8fd3e4d6
# Create Location
POST :host/v3/partner/:partner/location
Content-Type: application/yaml
X-Session-Token: :token
metadata:
name: :location
@@ -143,14 +154,17 @@ spec:
# Get all location
GET :host/v3/partner/:partner/location
Content-Type: application/yaml
X-Session-Token: :token
# Get single location
GET :host/v3/partner/:partner/location/:location
Content-Type: application/yaml
X-Session-Token: :token
# Update location info
PUT :host/v3/partner/:partner/location/:location
Content-Type: application/yaml
X-Session-Token: :token
metadata:
name: :location
@@ -171,6 +185,7 @@ DELETE :host/v3/partner/:partner/location/:location
# Create Cluster
POST :host/infra/v3/project/:project/cluster
Content-Type: application/yaml
X-Session-Token: :token
metadata:
name: :cluster
@@ -190,14 +205,17 @@ spec:
# Get all clusters
GET :host/infra/v3/project/:project/cluster
Content-Type: application/yaml
X-Session-Token: :token
# Get single cluster
GET :host/infra/v3/project/:project/cluster/:cluster
Content-Type: application/yaml
X-Session-Token: :token
# Update cluster info
PUT :host/infra/v3/project/:project/cluster/:cluster
Content-Type: application/yaml
X-Session-Token: :token
apiVersion: infra.k8smgmt.io/v3
kind: Cluster
@@ -230,6 +248,7 @@ DELETE :host/infra/v3/project/:project/cluster/:cluster
# Create user
POST :host/auth/v3/users
Content-Type: application/yaml
X-Session-Token: :token
metadata:
partner: :partner
@@ -242,6 +261,7 @@ spec:
# Create user with roles
POST :host/auth/v3/users
Content-Type: application/yaml
X-Session-Token: :token
metadata:
partner: :partner
@@ -251,21 +271,14 @@ spec:
firstName: John
lastName: Doe
projectNamespaceRoles:
- role: NAMESPACE_ADMIN
- role: :role
namespace: :namespace
project: :project
# Get all users
GET :host/auth/v3/users
Content-Type: application/yaml
# Get single user
GET :host/auth/v3/user/:user
Content-Type: application/yaml
# Update user info
PUT :host/auth/v3/user/:user
Content-Type: application/yaml
X-Session-Token: :token
metadata:
partner: :partner
@@ -275,10 +288,22 @@ spec:
firstName: John
lastName: Cena
projectNamespaceRoles:
- role: ADMIN
- role: :role
# Get all users
GET :host/auth/v3/users
Content-Type: application/yaml
X-Session-Token: :token
# Get single user
GET :host/auth/v3/user/:user
Content-Type: application/yaml
X-Session-Token: :token
# Delete single user
DELETE :host/auth/v3/user/:user
X-Session-Token: :token
## Groups
@@ -304,6 +329,7 @@ spec:
# Create group no namespace and project
POST :host/auth/v3/partner/:partner/organization/:org/groups
Content-Type: application/yaml
X-Session-Token: :token
metadata:
name: :group
@@ -320,6 +346,7 @@ spec:
# Update group to no namespace
PUT :host/auth/v3/partner/:partner/organization/:org/group/:group
Content-Type: application/yaml
X-Session-Token: :token
metadata:
name: :group
@@ -337,20 +364,24 @@ spec:
# Get all groups
GET :host/auth/v3/partner/:partner/organization/:org/groups
Content-Type: application/yaml
X-Session-Token: :token
# Get a group
GET :host/auth/v3/partner/:partner/organization/:org/group/:group
Content-Type: application/yaml
X-Session-Token: :token
# Delete a group
DELETE :host/auth/v3/partner/:partner/organization/:org/group/:group
Content-Type: application/yaml
X-Session-Token: :token
## Roles
# Create empty role
POST :host/auth/v3/partner/:partner/organization/:org/roles
Content-Type: application/yaml
X-Session-Token: :token
metadata:
name: :role
@@ -364,6 +395,7 @@ spec:
# Create role
POST :host/auth/v3/partner/:partner/organization/:org/roles
Content-Type: application/yaml
X-Session-Token: :token
metadata:
name: :role
@@ -372,7 +404,7 @@ metadata:
organization: :org
spec:
isGlobal: true
scope: cluster
scope: system
rolepermissions:
- account.read
- ops_star.all
@@ -380,6 +412,7 @@ spec:
# Update role
PUT :host/auth/v3/partner/:partner/organization/:org/role/:role
Content-Type: application/yaml
X-Session-Token: :token
metadata:
name: :role
@@ -388,21 +421,24 @@ metadata:
organization: :org
spec:
isGlobal: true
scope: cluster
scope: system
rolepermissions:
- account.read
- ops_star.all
# Get all roles
GET :host/auth/v3/partner/:partner/organization/:org/roles
Content-Type: application/yaml
X-Session-Token: :token
# Get a role
GET :host/auth/v3/partner/:partner/organization/:org/role/:role
Content-Type: application/yaml
X-Session-Token: :token
# Delete a role
DELETE :host/auth/v3/partner/:partner/organization/:org/role/:role
Content-Type: application/yaml
X-Session-Token: :token
## Rolepermission
@@ -410,6 +446,7 @@ Content-Type: application/yaml
# Get all rolepermissions
GET :host/auth/v3/rolepermissions
Content-Type: application/yaml
X-Session-Token: :token
## Audit
@@ -421,6 +458,7 @@ GET :host/event/v1/auditlog?filter.timefrom=now-1h
# Create partner
POST :host/auth/v3/partner
Content-Type: application/yaml
X-Session-Token: :token
metadata:
name: :partner
@@ -428,23 +466,39 @@ metadata:
spec:
host: "https://important.org"
# List organizations
GET :host/auth/v3/partner/:partner/organizations
# Get partner
GET :host/auth/v3/partner/:partner
Content-Type: application/yaml
X-Session-Token: :token
## Organization
# Create organization
POST :host/auth/v3/partner/:partner/organization
Content-Type: application/yaml
X-Session-Token: :token
metadata:
partner: :partner
name: :org
description: "Very first organization"
description: "Very first organizataion"
spec:
active: true
# List organizations
GET :host/auth/v3/partner/:partner/organizations
Content-Type: application/yaml
X-Session-Token: :token
# Get organization
GET :host/auth/v3/partner/:partner/organization/:org
Content-Type: application/yaml
X-Session-Token: :token
# Create project
POST :host/auth/v3/partner/:partner/organization/:org/project
Content-Type: application/yaml
X-Session-Token: :token
metadata:
name: :project
@@ -452,6 +506,11 @@ metadata:
spec:
active: true
# Get project
GET :host/auth/v3/partner/:partner/organization/:org/project/:project
Content-Type: application/yaml
X-Session-Token: :token
# Delete project
DELETE :host/auth/v3/partner/:partner/organization/:org/project/:project
X-Session-Token: :token
+20 -1
View File
@@ -3,10 +3,13 @@ package authv3
import (
"os"
"github.com/RafayLabs/rcloud-base/pkg/enforcer"
logv2 "github.com/RafayLabs/rcloud-base/pkg/log"
"github.com/RafayLabs/rcloud-base/pkg/service"
kclient "github.com/ory/kratos-client-go"
"github.com/uptrace/bun"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
var _log = logv2.GetLogger()
@@ -26,6 +29,7 @@ type Option struct {
type authContext struct {
kc *kclient.APIClient
ks service.ApiKeyService
as service.AuthzService
}
// NewAuthContext setup authentication and authorization dependencies.
@@ -35,6 +39,8 @@ func NewAuthContext(db *bun.DB) authContext {
kratosScheme string
kratosAddr string
)
// TODO: https://github.com/RafayLabs/prompt/pull/3#issuecomment-1073557206
// Where exactly should we be getting these values from?
if v, ok := os.LookupEnv("KRATOS_SCHEME"); ok {
kratosScheme = v
} else {
@@ -50,5 +56,18 @@ func NewAuthContext(db *bun.DB) authContext {
kratosConfig.Servers[0].URL = kratosScheme + "://" + kratosAddr
kc = kclient.NewAPIClient(kratosConfig)
return authContext{kc: kc, ks: service.NewApiKeyService(db)}
gormDb, err := gorm.Open(
postgres.New(postgres.Config{Conn: db.DB}),
&gorm.Config{},
)
if err != nil {
_log.Fatalw("unable to create db connection", "error", err)
}
enforcer, err := enforcer.NewCasbinEnforcer(gormDb).Init()
if err != nil {
_log.Fatalw("unable to init enforcer", "error", err)
}
as := service.NewAuthzService(db, enforcer)
return authContext{kc: kc, as: as, ks: service.NewApiKeyService(db)}
}
+33 -5
View File
@@ -2,15 +2,20 @@ package authv3
import (
context "context"
"strings"
"github.com/RafayLabs/rcloud-base/pkg/gateway"
commonpbv3 "github.com/RafayLabs/rcloud-base/proto/types/commonpb/v3"
commonv3 "github.com/RafayLabs/rcloud-base/proto/types/commonpb/v3"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
type hasMetadata interface {
GetMetadata() *commonv3.Metadata
}
func (ac authContext) NewAuthUnaryInterceptor(opt Option) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
// TODO: Optimize authentication for a session/gRPC
@@ -21,6 +26,27 @@ func (ac authContext) NewAuthUnaryInterceptor(opt Option) grpc.UnaryServerInterc
}
}
// We have to get the value of org, and project (namespace in
// future) as we will be using this inorder to authorize the
// user's access to different resources
var org string
var project string
resource, ok := req.(hasMetadata)
if ok {
meta := resource.GetMetadata()
org = meta.Organization
project = meta.Project
// overrides for picking up info when not in default metadata locations
// XXX: This requires any new items which does not follow metadata convention added here
switch strings.Split(info.FullMethod, "/")[1] {
case "rafay.dev.rpc.v3.Project":
project = meta.Name
case "rafay.dev.rpc.v3.Organization":
org = meta.Name
}
}
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Error(codes.InvalidArgument, "grpc metadata not exist")
@@ -43,11 +69,13 @@ func (ac authContext) NewAuthUnaryInterceptor(opt Option) grpc.UnaryServerInterc
if len(md.Get("grpcgateway-cookie")) != 0 {
cookie = md.Get("grpcgateway-cookie")[0]
}
acReq := &commonpbv3.IsRequestAllowedRequest{
acReq := &commonv3.IsRequestAllowedRequest{
Url: url,
Method: method,
XSessionToken: token,
Cookie: cookie,
Org: org,
Project: project,
}
res, err := ac.IsRequestAllowed(ctx, nil, acReq)
if err != nil {
@@ -57,12 +85,12 @@ func (ac authContext) NewAuthUnaryInterceptor(opt Option) grpc.UnaryServerInterc
s := res.GetStatus()
switch s {
case commonpbv3.RequestStatus_RequestAllowed:
case commonv3.RequestStatus_RequestAllowed:
ctx := NewSessionContext(ctx, res.SessionData)
return handler(ctx, req)
case commonpbv3.RequestStatus_RequestMethodOrURLNotAllowed:
case commonv3.RequestStatus_RequestMethodOrURLNotAllowed:
return nil, status.Error(codes.PermissionDenied, res.GetReason())
case commonpbv3.RequestStatus_RequestNotAuthenticated:
case commonv3.RequestStatus_RequestNotAuthenticated:
return nil, status.Error(codes.Unauthenticated, res.GetReason())
}
+41 -1
View File
@@ -3,19 +3,24 @@ package authv3
import (
"net/http"
"regexp"
"strings"
"github.com/RafayLabs/rcloud-base/internal/dao"
commonpbv3 "github.com/RafayLabs/rcloud-base/proto/types/commonpb/v3"
"github.com/google/uuid"
"github.com/uptrace/bun"
"github.com/urfave/negroni"
)
type authMiddleware struct {
db *bun.DB
ac authContext
opt Option
}
func NewAuthMiddleware(opt Option, db *bun.DB) negroni.Handler {
return &authMiddleware{
db: db,
ac: NewAuthContext(db),
opt: opt,
}
@@ -34,12 +39,48 @@ func (am *authMiddleware) ServeHTTP(rw http.ResponseWriter, r *http.Request, nex
return
}
}
// Auth is primarily done via grpc endpoints, this is only used
// for endoints which do not go through grpc As of now, it is just
// prompt.
var proj string
var org string
if strings.HasPrefix(r.URL.String(), "/v2/debug/prompt/project/") {
// /v2/debug/prompt/project/:project_id/cluster/:cluster_name
splits := strings.Split(r.URL.String(), "/")
if len(splits) > 5 {
projid, err := uuid.Parse(splits[5])
if err != nil {
_log.Errorf("Failed to authenticate: unable to parse project uuid")
http.Error(rw, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return
}
// What gets sent for project is the id unlike most other
// api routes, so we have to fetch the name as well as the
// org info for casbin
proj, org, err = dao.GetProjectOrganization(r.Context(), am.db, projid)
if err != nil {
_log.Errorf("Failed to authenticate: unable to find project")
http.Error(rw, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return
}
}
} else {
// The middleware to only used with routes which does not have
// a grpc and so fail for any other requests.
_log.Errorf("Failed to authenticate: not a prompt request")
http.Error(rw, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return
}
req := &commonpbv3.IsRequestAllowedRequest{
Url: r.URL.String(),
Method: r.Method,
XSessionToken: r.Header.Get("X-Session-Token"),
XApiKey: r.Header.Get("X-RAFAY-API-KEYID"),
Cookie: r.Header.Get("Cookie"),
Project: proj,
Org: org,
}
res, err := am.ac.IsRequestAllowed(r.Context(), r, req)
if err != nil {
@@ -64,5 +105,4 @@ func (am *authMiddleware) ServeHTTP(rw http.ResponseWriter, r *http.Request, nex
// status is unknown
http.Error(rw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
+36 -9
View File
@@ -7,6 +7,7 @@ import (
"strings"
rpcv3 "github.com/RafayLabs/rcloud-base/proto/rpc/user"
authzv1 "github.com/RafayLabs/rcloud-base/proto/types/authz"
commonv3 "github.com/RafayLabs/rcloud-base/proto/types/commonpb/v3"
"github.com/spacemonkeygo/httpsig"
)
@@ -25,10 +26,14 @@ func (ac *authContext) IsRequestAllowed(ctx context.Context, httpreq *http.Reque
}
// Authenticate request
err := ac.authenticate(ctx, httpreq, req, res)
err, succ := ac.authenticate(ctx, httpreq, req, res)
if err != nil {
return nil, err
}
// Don't bother checking authorization if athentication failed
if !succ {
return res, nil
}
// Authorize request
err = ac.authorize(ctx, req, res)
@@ -41,14 +46,14 @@ func (ac *authContext) IsRequestAllowed(ctx context.Context, httpreq *http.Reque
// authenticate validate whether the request is from a legitimate user
// and populate relevant information in res.
func (ac *authContext) authenticate(ctx context.Context, httpreq *http.Request, req *commonv3.IsRequestAllowedRequest, res *commonv3.IsRequestAllowedResponse) error {
func (ac *authContext) authenticate(ctx context.Context, httpreq *http.Request, req *commonv3.IsRequestAllowedRequest, res *commonv3.IsRequestAllowedResponse) (error, bool) {
if len(req.XApiKey) > 0 && len(req.XSessionToken) == 0 {
resp, err := ac.ks.GetByKey(ctx, &rpcv3.ApiKeyRequest{
Id: req.XApiKey,
})
if err != nil {
_log.Infow("unable to get api key", "key", req.XApiKey, "error", err)
return ErrInvalidAPIKey
return ErrInvalidAPIKey, false
}
var kg httpsig.KeyGetterFunc = func(id string) interface{} {
return []byte(resp.Secret)
@@ -58,7 +63,7 @@ func (ac *authContext) authenticate(ctx context.Context, httpreq *http.Request,
verifier.SetRequiredHeaders([]string{"content-md5", "date", "host", "nonce"})
err = verifier.Verify(httpreq)
if err != nil {
return ErrInvalidSignature
return ErrInvalidSignature, false
}
res.Status = commonv3.RequestStatus_RequestAllowed
res.SessionData.Username = resp.Name
@@ -73,9 +78,9 @@ func (ac *authContext) authenticate(ctx context.Context, httpreq *http.Request,
if strings.Contains(err.Error(), "401 Unauthorized") {
res.Status = commonv3.RequestStatus_RequestNotAuthenticated
res.Reason = "no or invalid credentials"
return nil
return nil, false
} else {
return err
return err, false
}
}
if session.GetActive() {
@@ -90,11 +95,33 @@ func (ac *authContext) authenticate(ctx context.Context, httpreq *http.Request,
res.Reason = "no active session"
}
}
return nil
return nil, true
}
// authorize performs authorization of the request and populate
// relevant information in res.
// authorize performs authorization of the request
func (ac *authContext) authorize(ctx context.Context, req *commonv3.IsRequestAllowedRequest, res *commonv3.IsRequestAllowedResponse) error {
// user,namespace,project,org,url(perm),method
// ones that don't have value should be "*"
proj := req.Project
if proj == "" {
proj = "*"
}
org := req.Org
if org == "" {
org = "*"
}
er := authzv1.EnforceRequest{
Params: []string{"u:" + res.SessionData.Username, "*", proj, org, req.Url, req.Method},
}
authenticated, err := ac.as.Enforce(ctx, &er)
if err != nil {
return err
}
if !authenticated.Res {
res.Status = commonv3.RequestStatus_RequestMethodOrURLNotAllowed
res.Reason = "not authorized to perform action"
return nil
}
return nil
}
+41 -42
View File
@@ -4,7 +4,7 @@ import (
"context"
"database/sql"
"fmt"
"strconv"
"strings"
"time"
"github.com/RafayLabs/rcloud-base/internal/dao"
@@ -77,53 +77,75 @@ func (s *groupService) createGroupRoleRelations(ctx context.Context, db bun.IDB,
// TODO: add transactions
projectNamespaceRoles := group.GetSpec().GetProjectNamespaceRoles()
var pgnrs []models.ProjectGroupNamespaceRole
var pgrs []models.ProjectGroupRole
var grs []models.GroupRole
var ps []*authzv1.Policy
for _, pnr := range projectNamespaceRoles {
role := pnr.GetRole()
entity, err := dao.GetIdByName(ctx, db, role, &models.Role{})
entity, err := dao.GetByName(ctx, db, role, &models.Role{})
if err != nil {
return &userv3.Group{}, fmt.Errorf("unable to find role '%v'", role)
}
var roleId uuid.UUID
var roleName string
var scope string
if rle, ok := entity.(*models.Role); ok {
roleId = rle.ID
roleName = rle.Name
scope = strings.ToLower(rle.Scope)
} else {
return &userv3.Group{}, fmt.Errorf("unable to find role '%v'", role)
}
project := pnr.GetProject()
org := group.GetMetadata().GetOrganization()
namespaceId := pnr.GetNamespace() // TODO: lookup id from name
switch {
case namespaceId != 0:
projectId, err := dao.GetProjectId(ctx, db, project)
if err != nil {
return &userv3.Group{}, fmt.Errorf("unable to find project '%v'", project)
}
pgnr := models.ProjectGroupNamespaceRole{
switch scope {
case "system":
gr := models.GroupRole{
Trash: false,
RoleId: roleId,
PartnerId: ids.Partner,
OrganizationId: ids.Organization,
GroupId: ids.Id,
ProjectId: projectId,
NamespaceId: namespaceId,
Active: true,
}
pgnrs = append(pgnrs, pgnr)
grs = append(grs, gr)
ps = append(ps, &authzv1.Policy{
Sub: "g:" + group.GetMetadata().GetName(),
Ns: strconv.FormatInt(namespaceId, 10),
Proj: project,
Ns: "*",
Proj: "*",
Org: "*",
Obj: role,
})
case "organization":
if org == "" {
return &userv3.Group{}, fmt.Errorf("no org name provided for role '%v'", roleName)
}
gr := models.GroupRole{
Trash: false,
RoleId: roleId,
PartnerId: ids.Partner,
OrganizationId: ids.Organization,
GroupId: ids.Id,
Active: true,
}
grs = append(grs, gr)
ps = append(ps, &authzv1.Policy{
Sub: "g:" + group.GetMetadata().GetName(),
Ns: "*",
Proj: "*",
Org: org,
Obj: role,
})
case project != "":
projectId, err := dao.GetProjectId(ctx, db, project)
case "project":
if org == "" {
return &userv3.Group{}, fmt.Errorf("no org name provided for role '%v'", roleName)
}
if project == "" {
return &userv3.Group{}, fmt.Errorf("no project name provided for role '%v'", roleName)
}
projectId, err := dao.GetProjectId(ctx, s.db, project)
if err != nil {
return &userv3.Group{}, fmt.Errorf("unable to find project '%v'", project)
}
@@ -145,29 +167,6 @@ func (s *groupService) createGroupRoleRelations(ctx context.Context, db bun.IDB,
Org: org,
Obj: role,
})
default:
gr := models.GroupRole{
Trash: false,
RoleId: roleId,
PartnerId: ids.Partner,
OrganizationId: ids.Organization,
GroupId: ids.Id,
Active: true,
}
grs = append(grs, gr)
ps = append(ps, &authzv1.Policy{
Sub: "g:" + group.GetMetadata().GetName(),
Ns: "*",
Proj: "*",
Org: org,
Obj: role,
})
}
}
if len(pgnrs) > 0 {
_, err := dao.Create(ctx, db, &pgnrs)
if err != nil {
return &userv3.Group{}, err
}
}
if len(pgrs) > 0 {
+17 -14
View File
@@ -216,9 +216,10 @@ func TestCreateGroupNoUsersWithRoles(t *testing.T) {
name string
roles []*userv3.ProjectNamespaceRole
dbname string
scope string
shouldfail bool
}{
{"just role", []*userv3.ProjectNamespaceRole{{Role: uuid.New().String()}}, "authsrv_grouprole", false},
{"just role", []*userv3.ProjectNamespaceRole{{Role: uuid.New().String()}}, "authsrv_grouprole", "system", false},
// {"just project", []*userv3.ProjectNamespaceRole{{Project: &projectid}}, "authsrv_grouprole", true}, // no role creation without role
// {"just namespace", []*userv3.ProjectNamespaceRole{{Namespace: &namespaceid}}, "authsrv_grouprole", true}, // no role creation without role,
// {"project and namespace", []*userv3.ProjectNamespaceRole{{Project: &projectid, Namespace: &namespaceid}}, "authsrv_grouprole", true}, // no role creation without role,
@@ -249,8 +250,8 @@ func TestCreateGroupNoUsersWithRoles(t *testing.T) {
mock.ExpectBegin()
mock.ExpectQuery(`INSERT INTO "authsrv_group"`).
WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(guuid))
mock.ExpectQuery(`SELECT "resourcerole"."id" FROM "authsrv_resourcerole" AS "resourcerole"`).
WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(pruuid))
mock.ExpectQuery(`SELECT "resourcerole"."id".* FROM "authsrv_resourcerole" AS "resourcerole"`).
WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id", "name", "scope"}).AddRow(pruuid, "role-name", tc.scope))
if tc.roles[0].Project != nil {
mock.ExpectQuery(`SELECT "project"."id" FROM "authsrv_project" AS "project"`).
WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(pruuid))
@@ -295,14 +296,15 @@ func TestCreateGroupWithUsersWithRoles(t *testing.T) {
users []string
roles []*userv3.ProjectNamespaceRole
dbname string
scope string
shouldfail bool
}{
{"just role", []string{"user-" + uuid.New().String()}, []*userv3.ProjectNamespaceRole{{Role: uuid.New().String()}}, "authsrv_grouprole", false},
{"just project", []string{"user-" + uuid.New().String()}, []*userv3.ProjectNamespaceRole{{Project: &projectid}}, "authsrv_grouprole", true}, // no role creation without role
{"just namespace", []string{"user-" + uuid.New().String()}, []*userv3.ProjectNamespaceRole{{Namespace: &namespaceid}}, "authsrv_grouprole", true}, // no role creation without role,
{"project and namespace", []string{"user-" + uuid.New().String()}, []*userv3.ProjectNamespaceRole{{Project: &projectid, Namespace: &namespaceid}}, "authsrv_grouprole", true}, // no role creation without role,
{"project and role", []string{"user-" + uuid.New().String()}, []*userv3.ProjectNamespaceRole{{Project: &projectid, Role: uuid.New().String()}}, "authsrv_projectgrouprole", false},
{"project role namespace", []string{"user-" + uuid.New().String()}, []*userv3.ProjectNamespaceRole{{Project: &projectid, Namespace: &namespaceid, Role: uuid.New().String()}}, "authsrv_projectgroupnamespacerole", false},
{"just role", []string{"user-" + uuid.New().String()}, []*userv3.ProjectNamespaceRole{{Role: uuid.New().String()}}, "authsrv_grouprole", "system", false},
{"just project", []string{"user-" + uuid.New().String()}, []*userv3.ProjectNamespaceRole{{Project: &projectid}}, "authsrv_grouprole", "system", true}, // no role creation without role
{"just namespace", []string{"user-" + uuid.New().String()}, []*userv3.ProjectNamespaceRole{{Namespace: &namespaceid}}, "authsrv_projectgrouprole", "project", true}, // no role creation without role,
{"project and namespace", []string{"user-" + uuid.New().String()}, []*userv3.ProjectNamespaceRole{{Project: &projectid, Namespace: &namespaceid}}, "authsrv_grouprole", "project", true}, // no role creation without role,
{"project and role", []string{"user-" + uuid.New().String()}, []*userv3.ProjectNamespaceRole{{Project: &projectid, Role: uuid.New().String()}}, "authsrv_projectgrouprole", "project", false},
// {"project role namespace", []string{"user-" + uuid.New().String()}, []*userv3.ProjectNamespaceRole{{Project: &projectid, Namespace: &namespaceid, Role: uuid.New().String()}}, "authsrv_projectgroupnamespacerole", false},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
@@ -334,8 +336,8 @@ func TestCreateGroupWithUsersWithRoles(t *testing.T) {
mock.ExpectQuery(`INSERT INTO "authsrv_groupaccount"`).
WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(uuid.New().String()))
mock.ExpectQuery(`SELECT "resourcerole"."id" FROM "authsrv_resourcerole" AS "resourcerole"`).
WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(pruuid))
mock.ExpectQuery(`SELECT "resourcerole"."id".* FROM "authsrv_resourcerole" AS "resourcerole"`).
WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id", "name", "scope"}).AddRow(pruuid, "role-name", tc.scope))
if tc.roles[0].Project != nil {
mock.ExpectQuery(`SELECT "project"."id" FROM "authsrv_project" AS "project"`).
WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(pruuid))
@@ -383,8 +385,9 @@ func TestUpdateGroupWithUsersWithRoles(t *testing.T) {
users []string
roles []*userv3.ProjectNamespaceRole
dbname string
scope string
}{
{"user role update", []string{"user-" + uuid.New().String()}, []*userv3.ProjectNamespaceRole{{Role: uuid.New().String()}}, "authsrv_grouprole"},
{"user role update", []string{"user-" + uuid.New().String()}, []*userv3.ProjectNamespaceRole{{Role: uuid.New().String()}}, "authsrv_grouprole", "system"},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
@@ -422,8 +425,8 @@ func TestUpdateGroupWithUsersWithRoles(t *testing.T) {
WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectExec(`UPDATE "authsrv_projectgroupnamespacerole" AS "projectgroupnamespacerole" SET trash = TRUE WHERE ."group_id" = '` + guuid).
WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectQuery(`SELECT "resourcerole"."id" FROM "authsrv_resourcerole" AS "resourcerole"`).
WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(pruuid))
mock.ExpectQuery(`SELECT "resourcerole"."id".* FROM "authsrv_resourcerole" AS "resourcerole"`).
WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id", "name", "scope"}).AddRow(pruuid, "role-name", tc.scope))
if tc.roles[0].Project != nil {
mock.ExpectQuery(`SELECT "project"."id" FROM "authsrv_project" AS "project"`).
WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(pruuid))
+1 -4
View File
@@ -131,10 +131,7 @@ func (s *roleService) Create(ctx context.Context, role *rolev3.Role) (*rolev3.Ro
}
scope := role.GetSpec().GetScope()
// since this is purely additional metadata at this point, we
// can kinda treat it as optional, and so we are allowing empty
// TODO: check if "" is valid
if !contains([]string{"system", "organization", "project", ""}, strings.ToLower(scope)) {
if !contains([]string{"system", "organization", "project"}, strings.ToLower(scope)) {
return nil, fmt.Errorf("unknown scope '%v'", scope)
}
+49 -44
View File
@@ -4,7 +4,7 @@ import (
"context"
"database/sql"
"fmt"
"strconv"
"strings"
"time"
"github.com/google/uuid"
@@ -102,55 +102,83 @@ func (s *userService) createUserRoleRelations(ctx context.Context, db bun.IDB, u
projectNamespaceRoles := user.GetSpec().GetProjectNamespaceRoles()
// TODO: add transactions
var panrs []models.ProjectAccountNamespaceRole
var pars []models.ProjectAccountResourcerole
var ars []models.AccountResourcerole
var ps []*authzv1.Policy
for _, pnr := range projectNamespaceRoles {
role := pnr.GetRole()
entity, err := dao.GetIdByName(ctx, db, role, &models.Role{})
entity, err := dao.GetByName(ctx, db, role, &models.Role{})
if err != nil {
return user, fmt.Errorf("unable to find role '%v'", role)
return &userv3.User{}, fmt.Errorf("unable to find role '%v'", role)
}
var roleId uuid.UUID
var roleName string
var scope string
if rle, ok := entity.(*models.Role); ok {
roleId = rle.ID
roleName = rle.Name
scope = strings.ToLower(rle.Scope)
} else {
return user, fmt.Errorf("unable to find role '%v'", role)
return &userv3.User{}, fmt.Errorf("unable to find role '%v'", role)
}
project := pnr.GetProject()
org := user.GetMetadata().GetOrganization()
namespaceId := pnr.GetNamespace() // TODO: lookup id from name
switch {
case pnr.Namespace != nil:
projectId, err := dao.GetProjectId(ctx, db, project)
if err != nil {
return user, fmt.Errorf("unable to find project '%v'", project)
}
panr := models.ProjectAccountNamespaceRole{
switch scope {
case "system":
ar := models.AccountResourcerole{
CreatedAt: time.Now(),
ModifiedAt: time.Now(),
Trash: false,
Default: true,
RoleId: roleId,
PartnerId: ids.Partner,
OrganizationId: ids.Organization, // Not really used
AccountId: ids.Id,
Active: true,
}
ars = append(ars, ar)
ps = append(ps, &authzv1.Policy{
Sub: "u:" + user.GetMetadata().GetName(),
Ns: "*",
Proj: "*",
Org: "*",
Obj: role,
})
case "organization":
if org == "" {
return &userv3.User{}, fmt.Errorf("no org name provided for role '%v'", roleName)
}
ar := models.AccountResourcerole{
CreatedAt: time.Now(),
ModifiedAt: time.Now(),
Trash: false,
Default: true,
RoleId: roleId,
PartnerId: ids.Partner,
OrganizationId: ids.Organization,
AccountId: ids.Id,
ProjectId: projectId,
NamespaceId: namespaceId,
Active: true,
}
panrs = append(panrs, panr)
ars = append(ars, ar)
ps = append(ps, &authzv1.Policy{
Sub: "u:" + user.GetMetadata().GetName(),
Ns: strconv.FormatInt(namespaceId, 10),
Proj: project,
Ns: "*",
Proj: "*",
Org: org,
Obj: role,
})
case project != "":
case "project":
if org == "" {
return &userv3.User{}, fmt.Errorf("no org name provided for role '%v'", roleName)
}
if project == "" {
return &userv3.User{}, fmt.Errorf("no project name provided for role '%v'", roleName)
}
projectId, err := dao.GetProjectId(ctx, db, project)
if err != nil {
return user, fmt.Errorf("unable to find project '%v'", project)
@@ -177,32 +205,9 @@ func (s *userService) createUserRoleRelations(ctx context.Context, db bun.IDB, u
Obj: role,
})
default:
ar := models.AccountResourcerole{
CreatedAt: time.Now(),
ModifiedAt: time.Now(),
Trash: false,
Default: true,
RoleId: roleId,
PartnerId: ids.Partner,
OrganizationId: ids.Organization,
AccountId: ids.Id,
Active: true,
if err != nil {
return user, fmt.Errorf("namespace specific roles are not handled")
}
ars = append(ars, ar)
ps = append(ps, &authzv1.Policy{
Sub: "u:" + user.GetMetadata().GetName(),
Ns: "*",
Proj: "*",
Org: org,
Obj: role,
})
}
}
if len(panrs) > 0 {
_, err := dao.Create(ctx, db, &panrs)
if err != nil {
return &userv3.User{}, err
}
}
if len(pars) > 0 {
+13 -11
View File
@@ -99,14 +99,16 @@ func TestCreateUserWithRole(t *testing.T) {
name string
roles []*userv3.ProjectNamespaceRole
dbname string
scope string
shouldfail bool
}{
{"just role", []*userv3.ProjectNamespaceRole{{Role: rname}}, "authsrv_accountresourcerole", false},
{"just project", []*userv3.ProjectNamespaceRole{{Project: &prname}}, "authsrv_accountrole", true}, // no role creation without role
{"just namespace", []*userv3.ProjectNamespaceRole{{Namespace: &namespaceid}}, "authsrv_accountrole", true}, // no role creation without role,
{"project and namespace", []*userv3.ProjectNamespaceRole{{Project: &prname, Namespace: &namespaceid}}, "authsrv_accountrole", true}, // no role creation without role,
{"project and role", []*userv3.ProjectNamespaceRole{{Project: &prname, Role: rname}}, "authsrv_projectaccountresourcerole", false},
{"project role namespace", []*userv3.ProjectNamespaceRole{{Project: &prname, Namespace: &namespaceid, Role: rname}}, "authsrv_projectaccountnamespacerole", false},
{"just role", []*userv3.ProjectNamespaceRole{{Role: rname}}, "authsrv_accountresourcerole", "system", false},
{"just role org scope", []*userv3.ProjectNamespaceRole{{Role: rname}}, "authsrv_accountresourcerole", "organization", false},
{"just project", []*userv3.ProjectNamespaceRole{{Project: &prname}}, "authsrv_accountrole", "system", true}, // no role creation without role
{"just namespace", []*userv3.ProjectNamespaceRole{{Namespace: &namespaceid}}, "authsrv_accountrole", "system", true}, // no role creation without role,
{"project and namespace", []*userv3.ProjectNamespaceRole{{Project: &prname, Namespace: &namespaceid}}, "authsrv_accountrole", "system", true}, // no role creation without role,
{"project and role", []*userv3.ProjectNamespaceRole{{Project: &prname, Role: rname}}, "authsrv_projectaccountresourcerole", "project", false},
{"project role namespace", []*userv3.ProjectNamespaceRole{{Project: &prname, Namespace: &namespaceid, Role: rname}}, "authsrv_projectaccountresourcerole", "project", false},
}
for _, tc := range tt {
@@ -129,8 +131,8 @@ func TestCreateUserWithRole(t *testing.T) {
WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(ouuid))
mock.ExpectBegin()
mock.ExpectQuery(`SELECT "resourcerole"."id" FROM "authsrv_resourcerole" AS "resourcerole"`).
WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(pruuid))
mock.ExpectQuery(`SELECT "resourcerole"."id".* FROM "authsrv_resourcerole" AS "resourcerole"`).
WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id", "name", "scope"}).AddRow(pruuid, "role-name", tc.scope))
if tc.roles[0].Project != nil {
mock.ExpectQuery(`SELECT "project"."id" FROM "authsrv_project" AS "project"`).
WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(pruuid))
@@ -198,11 +200,11 @@ func TestUpdateUser(t *testing.T) {
WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectExec(`UPDATE "authsrv_projectaccountnamespacerole" AS "projectaccountnamespacerole" SET trash = TRUE WHERE`).
WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectQuery(`SELECT "resourcerole"."id" FROM "authsrv_resourcerole" AS "resourcerole"`).
WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(pruuid))
mock.ExpectQuery(`SELECT "resourcerole"."id".* FROM "authsrv_resourcerole" AS "resourcerole"`).
WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id", "name", "scope"}).AddRow(pruuid, "role-name", "project"))
mock.ExpectQuery(`SELECT "project"."id" FROM "authsrv_project" AS "project"`).
WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(pruuid))
mock.ExpectQuery(`INSERT INTO "authsrv_projectaccountnamespacerole"`).
mock.ExpectQuery(`INSERT INTO "authsrv_projectaccountresourcerole"`).
WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(uuid.New().String()))
mock.ExpectCommit()