-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add policy file generation and check permission function (#53)
* 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
1 parent
866cf32
commit cd5d2cb
Showing
8 changed files
with
754 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.