Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add casbin extension #1

Merged
merged 3 commits into from
Feb 6, 2023
Merged
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
File renamed without changes.
91 changes: 90 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,90 @@
# .github
# Casbin (This is a community driven project)

Casbin is an authorization library that supports access control models like ACL, RBAC, ABAC.

This repo inspired by [fiber-casbin](https://github.com/gofiber/contrib/tree/main/casbin) and adapted to Hertz.

## Install

``` shell
go get github.com/hertz-contrib/casbin
```

## Import

```go
import "github.com/hertz-contrib/casbin"
```

## Example

```go
package main

import (
"context"
"log"

"github.com/cloudwego/hertz/pkg/app"
"github.com/cloudwego/hertz/pkg/app/server"
"github.com/hertz-contrib/casbin"
"github.com/hertz-contrib/sessions"
"github.com/hertz-contrib/sessions/cookie"
)

func main() {
h := server.Default()

// Using sessions and casbin.
store := cookie.NewStore([]byte("secret"))
h.Use(sessions.New("session", store))
auth, err := casbin.NewCasbinMiddleware("example/config/model.conf", "example/config/policy.csv", subjectFromSession)
if err != nil {
log.Fatal(err)
}

h.POST("/login", func(ctx context.Context, c *app.RequestContext) {
// Verify username and password.
// ...

// Store current subject in session
session := sessions.Default(c)
session.Set("name", "alice")
err := session.Save()
if err != nil {
log.Fatal(err)
}
c.String(200, "you login successfully")
})

h.GET("/book", auth.RequiresPermissions([]string{"book:read"}, casbin.WithLogic(casbin.AND)), func(ctx context.Context, c *app.RequestContext) {
c.String(200, "you read the book successfully")
})
h.POST("/book", auth.RequiresRoles([]string{"user"}, casbin.WithLogic(casbin.AND)), func(ctx context.Context, c *app.RequestContext) {
c.String(200, "you posted a book successfully")
})

h.Spin()
}

// subjectFromSession get subject from session.
func subjectFromSession(ctx context.Context, c *app.RequestContext) string {
// Get subject from session.
session := sessions.Default(c)
if subject, ok := session.Get("name").(string); !ok {
return ""
} else {
return subject
}
}
```

## Options

| Option | Default | Description |
|------------------|--------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------|
| Logic | `AND` | Logic is the logical operation (AND/OR) used in permission checks in case multiple permissions or roles are specified. |
| PermissionParser | `PermissionParserWithSeparator(":")` | PermissionParserFunc is used for parsing the permission to extract object and action usually. |
| Unauthorized | `func(ctx context.Context, c *app.RequestContext) { c.AbortWithStatus(consts.StatusUnauthorized) }` | Unauthorized defines the response body for unauthorized responses. |
| Forbidden | `func(ctx context.Context, c *app.RequestContext) { c.AbortWithStatus(consts.StatusForbidden) }` | Forbidden defines the response body for forbidden responses. |

181 changes: 181 additions & 0 deletions casbin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// Copyright 2023 CloudWeGo Authors
//
// 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 casbin

import (
"context"

"github.com/casbin/casbin/v2"
"github.com/cloudwego/hertz/pkg/app"
"github.com/cloudwego/hertz/pkg/protocol/consts"
)

type Middleware struct {
// Enforcer is the main interface for authorization enforcement and policy management.
enforcer *casbin.Enforcer
// LookupHandler is used to look up current subject in runtime.
// If it can not find anything, just return an empty string.
lookup LookupHandler
}

// NewCasbinMiddleware returns a new Middleware using Casbin's Enforcer internally.
//
// modelFile is the file path to Casbin model file e.g. path/to/rbac_model.conf.
// adapter can be a file or a DB adapter.
// lookup is a function that looks up the current subject in runtime and returns an empty string if nothing found.
func NewCasbinMiddleware(modelFile string, adapter interface{}, lookup LookupHandler) (*Middleware, error) {
e, err := casbin.NewEnforcer(modelFile, adapter)
if err != nil {
return nil, err
}

return NewCasbinMiddlewareFromEnforcer(e, lookup)
}

// NewCasbinMiddlewareFromEnforcer creates from given Enforcer.
func NewCasbinMiddlewareFromEnforcer(e *casbin.Enforcer, lookup LookupHandler) (*Middleware, error) {
if lookup == nil {
return nil, errLookupNil
}

return &Middleware{
enforcer: e,
lookup: lookup,
}, nil
}

// RequiresPermissions tries to find the current subject and determine if the
// subject has the required permissions according to predefined Casbin policies.
func (m *Middleware) RequiresPermissions(permissions []string, opts ...Option) app.HandlerFunc {
// Here we provide default options.
options := NewOptions(opts...)
return func(ctx context.Context, c *app.RequestContext) {
if len(permissions) == 0 {
c.Next(ctx)
return
}
// Look up current subject.
sub := m.lookup(ctx, c)
if sub == "" {
options.Unauthorized(ctx, c)
return
}
// Enforce Casbin policies.
if options.Logic == AND {
// Must pass all tests.
for _, permission := range permissions {
vals := append([]string{sub}, options.PermissionParser(permission)...)
if vals[0] == "" || vals[1] == "" {
// Can not handle any illegal permission strings.
c.AbortWithStatus(consts.StatusInternalServerError)
return
}
if ok, err := m.enforcer.Enforce(stringSliceToInterfaceSlice(vals)...); err != nil {
c.AbortWithStatus(consts.StatusInternalServerError)
return
} else if !ok {
options.Forbidden(ctx, c)
return
}
}
c.Next(ctx)
return
} else if options.Logic == OR {
// Need to pass at least one test.
for _, permission := range permissions {
values := append([]string{sub}, options.PermissionParser(permission)...)
if values[0] == "" || values[1] == "" {
// Can not handle any illegal permission strings.
c.AbortWithStatus(consts.StatusInternalServerError)
return
}
if ok, err := m.enforcer.Enforce(stringSliceToInterfaceSlice(values)...); err != nil {
c.AbortWithStatus(consts.StatusInternalServerError)
return
} else if ok {
c.Next(ctx)
return
}
}
options.Forbidden(ctx, c)
return
}
c.Next(ctx)
}
}

// RequiresRoles tries to find the current subject and determine if the
// subject has the required roles according to predefined Casbin policies.
func (m *Middleware) RequiresRoles(requiredRoles []string, opts ...Option) app.HandlerFunc {
// Here we provide default options.
options := NewOptions(opts...)
return func(ctx context.Context, c *app.RequestContext) {
if len(requiredRoles) == 0 {
c.Next(ctx)
return
}
// Look up current subject.
sub := m.lookup(ctx, c)
if sub == "" {
options.Unauthorized(ctx, c)
return
}
actualRoles, err := m.enforcer.GetRolesForUser(sub)
if err != nil {
c.AbortWithStatus(consts.StatusInternalServerError)
return
}

if options.Logic == AND {
// Must have all required roles.
for _, role := range requiredRoles {
if !containsString(actualRoles, role) {
options.Forbidden(ctx, c)
return
}
}
c.Next(ctx)
return
} else if options.Logic == OR {
// Need to have at least one of required roles.
for _, role := range requiredRoles {
if containsString(actualRoles, role) {
c.Next(ctx)
return
}
}
options.Forbidden(ctx, c)
return
}
c.Next(ctx)
}
}

func containsString(s []string, v string) bool {
for _, vv := range s {
if vv == v {
return true
}
}
return false
}

func stringSliceToInterfaceSlice(s []string) []interface{} {
res := make([]interface{}, len(s))
for i, v := range s {
res[i] = v
}
return res
}
Loading