Skip to content

Commit

Permalink
feat: add policy file generation and check permission function (#53)
Browse files Browse the repository at this point in the history
* add policy file generation

* fix policy file generation

* fix policy file generation

* add CheckPermission func

* fix CheckPermission func

* add UT

* go mod tidy

* fix test

* add permission_checker.go

* add permission_checker_utils.go

* fix CheckPermission

* fix CheckPermission

* fix CheckPermission
  • Loading branch information
akiyatomohiro authored Nov 7, 2024
1 parent 866cf32 commit cd5d2cb
Show file tree
Hide file tree
Showing 8 changed files with 754 additions and 1 deletion.
115 changes: 115 additions & 0 deletions cerbos/client/check.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package client

import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"

"github.com/reearth/reearthx/appx"
)

const (
checkPermissionQuery = `
query CheckPermission($input: CheckPermissionInput!) {
checkPermission(input: $input) {
allowed
}
}
`
graphqlPath = "/api/graphql"
)

type Client struct {
httpClient *http.Client
dashboardURL string
}

func NewClient(dashboardURL string) *Client {
return &Client{
httpClient: &http.Client{},
dashboardURL: dashboardURL,
}
}

type CheckPermissionInput struct {
Service string `json:"service"`
Resource string `json:"resource"`
Action string `json:"action"`
}

type CheckPermissionResponse struct {
Data struct {
CheckPermission struct {
Allowed bool `json:"allowed"`
} `json:"checkPermission"`
} `json:"data"`
}

type GraphQLQuery struct {
Query string `json:"query"`
Variables interface{} `json:"variables"`
}

func (c *Client) CheckPermission(ctx context.Context, authInfo *appx.AuthInfo, input CheckPermissionInput) (bool, error) {
if err := c.validateInput(authInfo); err != nil {
return false, err
}

req, err := c.createRequest(ctx, authInfo, input)
if err != nil {
return false, err
}

return c.executeRequest(req)
}

func (c *Client) validateInput(authInfo *appx.AuthInfo) error {
if authInfo == nil {
return fmt.Errorf("auth info is required")
}
return nil
}

func (c *Client) createRequest(ctx context.Context, authInfo *appx.AuthInfo, input CheckPermissionInput) (*http.Request, error) {
gqlRequest := GraphQLQuery{
Query: checkPermissionQuery,
Variables: map[string]interface{}{
"input": input,
},
}

requestBody, err := json.Marshal(gqlRequest)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}

req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.dashboardURL+graphqlPath, bytes.NewBuffer(requestBody))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}

c.setHeaders(req, authInfo)
return req, nil
}

func (c *Client) setHeaders(req *http.Request, authInfo *appx.AuthInfo) {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", authInfo.Token))
req.Header.Set("Content-Type", "application/json")
}

func (c *Client) executeRequest(req *http.Request) (bool, error) {
resp, err := c.httpClient.Do(req)
if err != nil {
return false, fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()

var response CheckPermissionResponse
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return false, fmt.Errorf("failed to decode response: %w", err)
}

return response.Data.CheckPermission.Allowed, nil
}
149 changes: 149 additions & 0 deletions cerbos/client/check_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package client

import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

"github.com/reearth/reearthx/appx"
"github.com/stretchr/testify/assert"
)

func TestClient_NewClient(t *testing.T) {
dashboardURL := "http://test-dashboard"
client := NewClient(dashboardURL)

assert.NotNil(t, client)
assert.Equal(t, dashboardURL, client.dashboardURL)
assert.NotNil(t, client.httpClient)
}

func TestClient_CheckPermission(t *testing.T) {
tests := []struct {
name string
authInfo *appx.AuthInfo
input CheckPermissionInput
serverStatus int
serverResp CheckPermissionResponse
wantAllowed bool
wantErr string
}{
{
name: "success - permission allowed",
authInfo: &appx.AuthInfo{
Token: "test-token",
},
input: CheckPermissionInput{
Service: "flow",
Resource: "project",
Action: "read",
},
serverStatus: http.StatusOK,
serverResp: CheckPermissionResponse{
Data: struct {
CheckPermission struct {
Allowed bool "json:\"allowed\""
} "json:\"checkPermission\""
}{
CheckPermission: struct {
Allowed bool "json:\"allowed\""
}{
Allowed: true,
},
},
},
wantAllowed: true,
},
{
name: "success - permission denied",
authInfo: &appx.AuthInfo{
Token: "test-token",
},
input: CheckPermissionInput{
Service: "flow",
Resource: "project",
Action: "write",
},
serverStatus: http.StatusOK,
serverResp: CheckPermissionResponse{
Data: struct {
CheckPermission struct {
Allowed bool "json:\"allowed\""
} "json:\"checkPermission\""
}{
CheckPermission: struct {
Allowed bool "json:\"allowed\""
}{
Allowed: false,
},
},
},
wantAllowed: false,
},
{
name: "error - nil auth info",
authInfo: nil,
input: CheckPermissionInput{
Service: "flow",
Resource: "project",
Action: "read",
},
wantErr: "auth info is required",
},
{
name: "error - server error",
authInfo: &appx.AuthInfo{
Token: "test-token",
},
input: CheckPermissionInput{
Service: "flow",
Resource: "project",
Action: "read",
},
serverStatus: http.StatusInternalServerError,
wantErr: "failed to decode response",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "POST", r.Method)
assert.Equal(t, "/api/graphql", r.URL.Path)
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
if tt.authInfo != nil {
assert.Equal(t, "Bearer "+tt.authInfo.Token, r.Header.Get("Authorization"))
}

var gqlRequest GraphQLQuery
err := json.NewDecoder(r.Body).Decode(&gqlRequest)
assert.NoError(t, err)
assert.Contains(t, gqlRequest.Query, "query CheckPermission")
assert.Contains(t, gqlRequest.Query, "checkPermission")

w.WriteHeader(tt.serverStatus)
if tt.serverStatus == http.StatusOK {
if err := json.NewEncoder(w).Encode(tt.serverResp); err != nil {
t.Fatalf("failed to encode response: %v", err)
return
}
}
}))
defer server.Close()

client := NewClient(server.URL)
allowed, err := client.CheckPermission(context.Background(), tt.authInfo, tt.input)

if tt.wantErr != "" {
assert.Error(t, err)
assert.Contains(t, err.Error(), tt.wantErr)
return
}

assert.NoError(t, err)
assert.Equal(t, tt.wantAllowed, allowed)
})
}
}
39 changes: 39 additions & 0 deletions cerbos/client/permission_checker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package client

import (
"context"
"fmt"

"github.com/reearth/reearthx/appx"
)

type PermissionChecker struct {
Service string
DashboardURL string
}

func NewPermissionChecker(service string, dashboardURL string) *PermissionChecker {
return &PermissionChecker{
Service: service,
DashboardURL: dashboardURL,
}
}

func (p *PermissionChecker) CheckPermission(ctx context.Context, authInfo *appx.AuthInfo, resource string, action string) (bool, error) {
if p == nil {
return false, fmt.Errorf("permission checker not found")
}

if authInfo == nil {
return false, fmt.Errorf("auth info not found")
}

input := CheckPermissionInput{
Service: p.Service,
Resource: resource,
Action: action,
}

client := NewClient(p.DashboardURL)
return client.CheckPermission(ctx, authInfo, input)
}
46 changes: 46 additions & 0 deletions cerbos/generator/builder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package generator

type ResourceDefinition struct {
Resource string
Actions []ActionDefinition
}

type ActionDefinition struct {
Action string
Roles []string
}

type ResourceBuilder struct {
serviceName string
resources map[string][]ActionDefinition
}

func NewResourceBuilder(serviceName string) *ResourceBuilder {
return &ResourceBuilder{
serviceName: serviceName,
resources: make(map[string][]ActionDefinition),
}
}

func NewActionDefinition(action string, roles []string) ActionDefinition {
return ActionDefinition{
Action: action,
Roles: roles,
}
}

func (b *ResourceBuilder) AddResource(resource string, actions []ActionDefinition) *ResourceBuilder {
b.resources[resource] = actions
return b
}

func (b *ResourceBuilder) Build() []ResourceDefinition {
result := make([]ResourceDefinition, 0, len(b.resources))
for resource, actions := range b.resources {
result = append(result, ResourceDefinition{
Resource: b.serviceName + ":" + resource,
Actions: actions,
})
}
return result
}
Loading

0 comments on commit cd5d2cb

Please sign in to comment.