Browse Source

Support Policy Attachment for Object Store Users (#7981)

* Implement Policy Attachment support for Object Store Users

- Added policy_names field to iam.proto and regenerated protos.
- Updated S3 API and IAM integration to support direct policy evaluation for users.
- Enhanced Admin UI to allow attaching policies to users via modals.
- Renamed 'policies' to 'policy_names' to clarify that it stores identifiers.
- Fixed syntax error in user_management.go.

* Fix policy dropdown not populating

The API returns {policies: [...]} but JavaScript was treating response as direct array.
Updated loadPolicies() to correctly access data.policies property.

* Add null safety checks for policy dropdowns

Added checks to prevent "undefined" errors when:
- Policy select elements don't exist
- Policy dropdowns haven't loaded yet
- User is being edited before policies are loaded

* Fix policy dropdown by using correct JSON field name

JSON response has lowercase 'name' field but JavaScript was accessing 'Name'.
Changed policy.Name to policy.name to match the IAMPolicy JSON structure.

* Fix policy names not being saved on user update

Changed condition from len(req.PolicyNames) > 0 to req.PolicyNames != nil
to ensure policy names are always updated when present in the request,
even if it's an empty array (to allow clearing policies).

* Add debug logging for policy names update flow

Added console.log in frontend and glog in backend to trace
policy_names data through the update process.

* Temporarily disable auto-reload for debugging

Commented out window.location.reload() so console logs are visible
when updating a user.

* Add detailed debug logging and alert for policy selection

Added console.log for each step and an alert to show policy_names value
to help diagnose why it's not being included in the request.

* Regenerate templ files for object_store_users

Ran templ generate to ensure _templ.go files are up to date with
the latest .templ changes including debug logging.

* Remove debug logging and restore normal functionality

Cleaned up temporary debug code (console.log and alert statements)
and re-enabled automatic page reload after user update.

* Add step-by-step alert debugging for policy update

Added 5 alert checkpoints to trace policy data through the update flow:
1. Check if policiesSelect element exists
2. Show selected policy values
3. Show userData.policy_names
4. Show full request body
5. Confirm server response

Temporarily disabled auto-reload to see alerts.

* Add version check alert on page load

Added alert on DOMContentLoaded to verify new JavaScript is being executed
and not cached by the browser.

* Compile templates using make

Ran make to compile all template files and install the weed binary.

* Add button click detection and make handleUpdateUser global

- Added inline alert on button click to verify click is detected
- Made handleUpdateUser a window-level function to ensure it's accessible
- Added alert at start of handleUpdateUser function

* Fix handleUpdateUser scope issue - remove duplicate definition

Removed duplicate function definition that was inside DOMContentLoaded.
Now handleUpdateUser is defined only once in global scope (line 383)
making it accessible when button onclick fires.

* Remove all duplicate handleUpdateUser definitions

Now handleUpdateUser is defined only once at the very top of the script
block (line 352), before DOMContentLoaded, ensuring it's available when
the button onclick fires.

* Add function existence check and error catching

Added alerts to check if handleUpdateUser is defined and wrapped
the function call in try-catch to capture any JavaScript errors.
Also added console.log statements to verify function definition.

* Simplify handleUpdateUser to non-async for testing

Removed async/await and added early return to test if function
can be called at all. This will help identify if async is causing
the issue.

* Add cache-control headers to prevent browser caching

Added no-cache headers to ShowObjectStoreUsers handler to prevent
aggressive browser caching of inline JavaScript in the HTML page.

* Fix syntax error - make handleUpdateUser async

Changed function back to async to fix 'await is only valid in async functions' error.
The cache-control headers are working - browser is now loading new code.

* Update version check to v3 to verify cache busting

Changed version alert to 'v3 - WITH EARLY RETURN' to confirm
the new code with early return statement is being loaded.

* Remove all debug code - clean implementation

Removed all alerts, console.logs, and test code.
Implemented clean policy update functionality with proper error handling.

* Add ETag header for cache-busting and update walkthrough

* Fix policy pre-selection in Edit User modal

- Updated admin.js editUser function to pre-select policies
- Root cause: duplicate editUser in admin.js overwrote inline version
- Added policy pre-selection logic to match inline template
- Verified working in browser: policies now pre-select correctly

* Fix policy persistence in handleUpdateUser

- Added policy_names field to userData payload in handleUpdateUser
- Policies were being lost because handleUpdateUser only sent email and actions
- Now collects selected policies from editPolicies dropdown
- Verified working: policies persist correctly across updates

* Fix XSS vulnerability in access keys display

- Escape HTML in access key display using escapeHtml utility
- Replace inline onclick handlers with data attributes
- Add event delegation for delete access key buttons
- Prevents script injection via malicious access key values

* Fix additional XSS vulnerabilities in user details display

- Escape HTML in actions badges (line 626)
- Escape HTML in policy_names badges (line 636)
- Prevents script injection via malicious action or policy names

* Fix XSS vulnerability in loadPolicies function

- Replace innerHTML string concatenation with DOM API
- Use createElement and textContent for safe policy name insertion
- Prevents script injection via malicious policy names
- Apply same pattern to both create and edit select elements

* Remove debug logging from UpdateObjectStoreUser

- Removed glog.V(0) debug statements
- Clean up temporary debugging code before production

* Remove duplicate handleUpdateUser function

- Removed inline handleUpdateUser that duplicated admin.js logic
- Removed debug console.log statement
- admin.js version is now the single source of truth
- Eliminates maintenance burden of keeping two versions in sync

* Refine user management and address code review feedback

- Preserve PolicyNames in UpdateUserPolicies
- Allow clearing actions in UpdateObjectStoreUser by checking for nil
- Remove version comment from object_store_users.templ
- Refactor loadPolicies for DRYness using cloneNode while keeping DOM API security

* IAM Authorization for Static Access Keys

* verified XSS Fixes in Templates

* fix div
pull/7983/head
Chris Lu 3 weeks ago
committed by GitHub
parent
commit
e67973dc53
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 16
      weed/admin/dash/admin_data.go
  2. 20
      weed/admin/dash/user_management.go
  3. 6
      weed/admin/handlers/user_handlers.go
  4. 504
      weed/admin/static/js/admin.js
  5. 139
      weed/admin/view/app/object_store_users.templ
  6. 2
      weed/admin/view/app/object_store_users_templ.go
  7. 33
      weed/iam/integration/iam_manager.go
  8. 2
      weed/pb/filer_pb/filer.pb.go
  9. 2
      weed/pb/filer_pb/filer_grpc.pb.go
  10. 1
      weed/pb/iam.proto
  11. 13
      weed/pb/iam_pb/iam.pb.go
  12. 2
      weed/pb/master_pb/master.pb.go
  13. 2
      weed/pb/master_pb/master_grpc.pb.go
  14. 2
      weed/pb/mount_pb/mount.pb.go
  15. 2
      weed/pb/mount_pb/mount_grpc.pb.go
  16. 2
      weed/pb/mq_agent_pb/mq_agent.pb.go
  17. 2
      weed/pb/mq_agent_pb/mq_agent_grpc.pb.go
  18. 2
      weed/pb/mq_pb/mq_broker.pb.go
  19. 2
      weed/pb/mq_pb/mq_broker_grpc.pb.go
  20. 2
      weed/pb/remote_pb/remote.pb.go
  21. 2
      weed/pb/s3_pb/s3.pb.go
  22. 2
      weed/pb/s3_pb/s3_grpc.pb.go
  23. 2
      weed/pb/schema_pb/mq_schema.pb.go
  24. 2
      weed/pb/volume_server_pb/volume_server.pb.go
  25. 2
      weed/pb/volume_server_pb/volume_server_grpc.pb.go
  26. 2
      weed/pb/worker_pb/worker.pb.go
  27. 2
      weed/pb/worker_pb/worker_grpc.pb.go
  28. 18
      weed/s3api/auth_credentials.go
  29. 5
      weed/s3api/s3_iam_middleware.go

16
weed/admin/dash/admin_data.go

@ -37,6 +37,7 @@ type ObjectStoreUser struct {
AccessKey string `json:"access_key"` AccessKey string `json:"access_key"`
SecretKey string `json:"secret_key"` SecretKey string `json:"secret_key"`
Permissions []string `json:"permissions"` Permissions []string `json:"permissions"`
PolicyNames []string `json:"policy_names"`
} }
type ObjectStoreUsersData struct { type ObjectStoreUsersData struct {
@ -52,11 +53,13 @@ type CreateUserRequest struct {
Email string `json:"email"` Email string `json:"email"`
Actions []string `json:"actions"` Actions []string `json:"actions"`
GenerateKey bool `json:"generate_key"` GenerateKey bool `json:"generate_key"`
PolicyNames []string `json:"policy_names"`
} }
type UpdateUserRequest struct { type UpdateUserRequest struct {
Email string `json:"email"`
Actions []string `json:"actions"`
Email string `json:"email"`
Actions []string `json:"actions"`
PolicyNames []string `json:"policy_names"`
} }
type UpdateUserPoliciesRequest struct { type UpdateUserPoliciesRequest struct {
@ -70,10 +73,11 @@ type AccessKeyInfo struct {
} }
type UserDetails struct { type UserDetails struct {
Username string `json:"username"`
Email string `json:"email"`
Actions []string `json:"actions"`
AccessKeys []AccessKeyInfo `json:"access_keys"`
Username string `json:"username"`
Email string `json:"email"`
Actions []string `json:"actions"`
PolicyNames []string `json:"policy_names"`
AccessKeys []AccessKeyInfo `json:"access_keys"`
} }
type FilerNode struct { type FilerNode struct {

20
weed/admin/dash/user_management.go

@ -21,8 +21,9 @@ func (s *AdminServer) CreateObjectStoreUser(req CreateUserRequest) (*ObjectStore
// Create new identity // Create new identity
newIdentity := &iam_pb.Identity{ newIdentity := &iam_pb.Identity{
Name: req.Username,
Actions: req.Actions,
Name: req.Username,
Actions: req.Actions,
PolicyNames: req.PolicyNames,
} }
// Add account if email is provided // Add account if email is provided
@ -63,6 +64,7 @@ func (s *AdminServer) CreateObjectStoreUser(req CreateUserRequest) (*ObjectStore
AccessKey: accessKey, AccessKey: accessKey,
SecretKey: secretKey, SecretKey: secretKey,
Permissions: req.Actions, Permissions: req.Actions,
PolicyNames: req.PolicyNames,
} }
return user, nil return user, nil
@ -91,12 +93,17 @@ func (s *AdminServer) UpdateObjectStoreUser(username string, req UpdateUserReque
Account: identity.Account, Account: identity.Account,
Credentials: identity.Credentials, Credentials: identity.Credentials,
Actions: identity.Actions, Actions: identity.Actions,
PolicyNames: identity.PolicyNames,
} }
// Update actions if provided // Update actions if provided
if len(req.Actions) > 0 {
if req.Actions != nil {
updatedIdentity.Actions = req.Actions updatedIdentity.Actions = req.Actions
} }
// Always update policy names when present in request (even if empty to allow clearing)
if req.PolicyNames != nil {
updatedIdentity.PolicyNames = req.PolicyNames
}
// Update email if provided // Update email if provided
if req.Email != "" { if req.Email != "" {
@ -120,6 +127,7 @@ func (s *AdminServer) UpdateObjectStoreUser(username string, req UpdateUserReque
Username: username, Username: username,
Email: req.Email, Email: req.Email,
Permissions: updatedIdentity.Actions, Permissions: updatedIdentity.Actions,
PolicyNames: updatedIdentity.PolicyNames,
} }
// Get first access key for display // Get first access key for display
@ -169,8 +177,9 @@ func (s *AdminServer) GetObjectStoreUserDetails(username string) (*UserDetails,
} }
details := &UserDetails{ details := &UserDetails{
Username: username,
Actions: identity.Actions,
Username: username,
Actions: identity.Actions,
PolicyNames: identity.PolicyNames,
} }
// Set email from account if available // Set email from account if available
@ -295,6 +304,7 @@ func (s *AdminServer) UpdateUserPolicies(username string, actions []string) erro
Account: identity.Account, Account: identity.Account,
Credentials: identity.Credentials, Credentials: identity.Credentials,
Actions: actions, Actions: actions,
PolicyNames: identity.PolicyNames,
} }
// Update user using credential manager // Update user using credential manager

6
weed/admin/handlers/user_handlers.go

@ -1,6 +1,7 @@
package handlers package handlers
import ( import (
"fmt"
"net/http" "net/http"
"time" "time"
@ -29,7 +30,12 @@ func (h *UserHandlers) ShowObjectStoreUsers(c *gin.Context) {
usersData := h.getObjectStoreUsersData(c) usersData := h.getObjectStoreUsersData(c)
// Render HTML template // Render HTML template
// Add cache-control headers to prevent browser caching of inline JavaScript
c.Header("Content-Type", "text/html") c.Header("Content-Type", "text/html")
c.Header("Cache-Control", "no-cache, no-store, must-revalidate")
c.Header("Pragma", "no-cache")
c.Header("Expires", "0")
c.Header("ETag", fmt.Sprintf("\"%d\"", time.Now().Unix()))
usersComponent := app.ObjectStoreUsers(usersData) usersComponent := app.ObjectStoreUsers(usersData)
layoutComponent := layout.Layout(c, usersComponent) layoutComponent := layout.Layout(c, usersComponent)
err := layoutComponent.Render(c.Request.Context(), c.Writer) err := layoutComponent.Render(c.Request.Context(), c.Writer)

504
weed/admin/static/js/admin.js
File diff suppressed because it is too large
View File

139
weed/admin/view/app/object_store_users.templ

@ -223,6 +223,13 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
</select> </select>
<small class="form-text text-muted">Hold Ctrl/Cmd to select multiple permissions</small> <small class="form-text text-muted">Hold Ctrl/Cmd to select multiple permissions</small>
</div> </div>
<div class="mb-3">
<label for="policies" class="form-label">Attached Policies</label>
<select multiple class="form-control" id="policies" name="policies" size="5">
<!-- Options loaded dynamically -->
</select>
<small class="form-text text-muted">Hold Ctrl/Cmd to select multiple policies</small>
</div>
<div class="mb-3 form-check"> <div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="generateKey" name="generateKey" checked> <input type="checkbox" class="form-check-input" id="generateKey" name="generateKey" checked>
<label class="form-check-label" for="generateKey"> <label class="form-check-label" for="generateKey">
@ -275,6 +282,12 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
</optgroup> </optgroup>
</select> </select>
</div> </div>
<div class="mb-3">
<label for="editPolicies" class="form-label">Attached Policies</label>
<select multiple class="form-control" id="editPolicies" name="policies" size="5">
<!-- Options loaded dynamically -->
</select>
</div>
</form> </form>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
@ -336,6 +349,7 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
<!-- JavaScript for user management --> <!-- JavaScript for user management -->
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Event delegation for user action buttons // Event delegation for user action buttons
document.addEventListener('click', function(e) { document.addEventListener('click', function(e) {
const button = e.target.closest('[data-action]'); const button = e.target.closest('[data-action]');
@ -359,8 +373,67 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
break; break;
} }
}); });
// Event delegation for delete access key buttons
document.addEventListener('click', function(e) {
const deleteBtn = e.target.closest('.delete-access-key-btn');
if (deleteBtn) {
const username = deleteBtn.getAttribute('data-username');
const accessKey = deleteBtn.getAttribute('data-access-key');
deleteAccessKey(username, accessKey);
}
});
// Load policies for dropdowns
loadPolicies();
}); });
// Load policies
async function loadPolicies() {
try {
const response = await fetch('/api/object-store/policies');
if (response.ok) {
const data = await response.json();
const policies = data.policies || [];
const createSelect = document.getElementById('policies');
const editSelect = document.getElementById('editPolicies');
// Check if elements exist
if (!createSelect || !editSelect) {
console.warn('Policy select elements not found');
return;
}
// Clear existing options
createSelect.innerHTML = '';
editSelect.innerHTML = '';
if (policies && policies.length > 0) {
policies.forEach(policy => {
const option = document.createElement('option');
option.value = policy.name;
option.textContent = policy.name;
createSelect.appendChild(option);
editSelect.appendChild(option.cloneNode(true));
});
} else {
const emptyOption = document.createElement('option');
emptyOption.disabled = true;
emptyOption.textContent = 'No policies available';
createSelect.appendChild(emptyOption);
editSelect.appendChild(emptyOption.cloneNode(true));
}
} else {
const error = await response.json().catch(() => ({}));
showErrorMessage('Failed to load policies: ' + (error.error || 'Unknown error'));
}
} catch (error) {
console.error('Error loading policies:', error);
showErrorMessage('Failed to load policies: ' + error.message);
}
}
// Show user details modal // Show user details modal
async function showUserDetails(username) { async function showUserDetails(username) {
try { try {
@ -395,6 +468,14 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
Array.from(actionsSelect.options).forEach(option => { Array.from(actionsSelect.options).forEach(option => {
option.selected = user.actions && user.actions.includes(option.value); option.selected = user.actions && user.actions.includes(option.value);
}); });
// Set selected policies
const policiesSelect = document.getElementById('editPolicies');
if (policiesSelect) {
Array.from(policiesSelect.options).forEach(option => {
option.selected = user.policy_names && user.policy_names.includes(option.value);
});
}
// Show modal // Show modal
const modal = new bootstrap.Modal(document.getElementById('editUserModal')); const modal = new bootstrap.Modal(document.getElementById('editUserModal'));
@ -458,6 +539,7 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
username: formData.get('username'), username: formData.get('username'),
email: formData.get('email'), email: formData.get('email'),
actions: Array.from(document.getElementById('actions').selectedOptions).map(option => option.value), actions: Array.from(document.getElementById('actions').selectedOptions).map(option => option.value),
policy_names: Array.from(document.getElementById('policies').selectedOptions).map(option => option.value),
generate_key: document.getElementById('generateKey').checked generate_key: document.getElementById('generateKey').checked
}; };
@ -494,41 +576,6 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
} }
} }
// Handle update user form submission
async function handleUpdateUser() {
const username = document.getElementById('editUsername').value;
const formData = new FormData(document.getElementById('editUserForm'));
const userData = {
email: formData.get('email'),
actions: Array.from(document.getElementById('editActions').selectedOptions).map(option => option.value)
};
try {
const response = await fetch(`/api/users/${username}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(userData)
});
if (response.ok) {
showSuccessMessage('User updated successfully');
// Close modal and refresh page
const modal = bootstrap.Modal.getInstance(document.getElementById('editUserModal'));
modal.hide();
setTimeout(() => window.location.reload(), 1000);
} else {
const error = await response.json();
showErrorMessage('Failed to update user: ' + (error.error || 'Unknown error'));
}
} catch (error) {
console.error('Error updating user:', error);
showErrorMessage('Failed to update user: ' + error.message);
}
}
// Create user details content // Create user details content
function createUserDetailsContent(user) { function createUserDetailsContent(user) {
@ -545,17 +592,27 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
detailsHtml += '<div class="mb-3">'; detailsHtml += '<div class="mb-3">';
if (user.actions && user.actions.length > 0) { if (user.actions && user.actions.length > 0) {
detailsHtml += user.actions.map(function(action) { detailsHtml += user.actions.map(function(action) {
return '<span class="badge bg-info me-1">' + action + '</span>';
return '<span class="badge bg-info me-1">' + escapeHtml(action) + '</span>';
}).join(''); }).join('');
} else { } else {
detailsHtml += '<span class="text-muted">No permissions assigned</span>'; detailsHtml += '<span class="text-muted">No permissions assigned</span>';
} }
detailsHtml += '</div>';
detailsHtml += '<h6 class="text-muted">Access Keys</h6>';
detailsHtml += '</div>';
detailsHtml += '<h6 class="text-muted">Attached Policy Names</h6>';
detailsHtml += '<div class="mb-3">';
if (user.policy_names && user.policy_names.length > 0) {
detailsHtml += user.policy_names.map(function(policy) {
return '<span class="badge bg-secondary me-1">' + escapeHtml(policy) + '</span>';
}).join('');
} else {
detailsHtml += '<span class="text-muted">No policies attached</span>';
}
detailsHtml += '</div>';
detailsHtml += '<h6 class="text-muted">Access Keys</h6>';
if (user.access_keys && user.access_keys.length > 0) { if (user.access_keys && user.access_keys.length > 0) {
detailsHtml += '<div class="mb-2">'; detailsHtml += '<div class="mb-2">';
user.access_keys.forEach(function(key) { user.access_keys.forEach(function(key) {
detailsHtml += '<div><code class="text-muted">' + key.access_key + '</code></div>';
detailsHtml += '<div><code class="text-muted">' + escapeHtml(key.access_key) + '</code></div>';
}); });
detailsHtml += '</div>'; detailsHtml += '</div>';
} else { } else {
@ -579,10 +636,10 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
user.access_keys.forEach(function(key) { user.access_keys.forEach(function(key) {
keysHtml += '<tr>'; keysHtml += '<tr>';
keysHtml += '<td><code>' + key.access_key + '</code></td>';
keysHtml += '<td><code>' + escapeHtml(key.access_key) + '</code></td>';
keysHtml += '<td><span class="badge bg-success">Active</span></td>'; keysHtml += '<td><span class="badge bg-success">Active</span></td>';
keysHtml += '<td>'; keysHtml += '<td>';
keysHtml += '<button class="btn btn-outline-danger btn-sm" onclick="deleteAccessKey(\'' + user.username + '\', \'' + key.access_key + '\')">';
keysHtml += '<button class="btn btn-outline-danger btn-sm delete-access-key-btn" data-username="' + escapeHtml(user.username) + '" data-access-key="' + escapeHtml(key.access_key) + '">';
keysHtml += '<i class="fas fa-trash"></i> Delete'; keysHtml += '<i class="fas fa-trash"></i> Delete';
keysHtml += '</button>'; keysHtml += '</button>';
keysHtml += '</td>'; keysHtml += '</td>';

2
weed/admin/view/app/object_store_users_templ.go
File diff suppressed because it is too large
View File

33
weed/iam/integration/iam_manager.go

@ -77,6 +77,9 @@ type ActionRequest struct {
// RequestContext contains additional request information // RequestContext contains additional request information
RequestContext map[string]interface{} `json:"requestContext,omitempty"` RequestContext map[string]interface{} `json:"requestContext,omitempty"`
// PolicyNames to evaluate (overrides role-based policies if present)
PolicyNames []string `json:"policyNames,omitempty"`
} }
// NewIAMManager creates a new IAM manager // NewIAMManager creates a new IAM manager
@ -281,14 +284,32 @@ func (m *IAMManager) IsActionAllowed(ctx context.Context, request *ActionRequest
return false, fmt.Errorf("IAM manager not initialized") return false, fmt.Errorf("IAM manager not initialized")
} }
// Validate session token first (skip for OIDC tokens which are already validated)
if !isOIDCToken(request.SessionToken) {
// Validate session token if present (skip for OIDC tokens which are already validated,
// and skip for empty tokens which represent static access keys)
if request.SessionToken != "" && !isOIDCToken(request.SessionToken) {
_, err := m.stsService.ValidateSessionToken(ctx, request.SessionToken) _, err := m.stsService.ValidateSessionToken(ctx, request.SessionToken)
if err != nil { if err != nil {
return false, fmt.Errorf("invalid session: %w", err) return false, fmt.Errorf("invalid session: %w", err)
} }
} }
// Create evaluation context
evalCtx := &policy.EvaluationContext{
Principal: request.Principal,
Action: request.Action,
Resource: request.Resource,
RequestContext: request.RequestContext,
}
// If explicit policy names are provided (e.g. from user identity), evaluate them directly
if len(request.PolicyNames) > 0 {
result, err := m.policyEngine.Evaluate(ctx, "", evalCtx, request.PolicyNames)
if err != nil {
return false, fmt.Errorf("policy evaluation failed: %w", err)
}
return result.Effect == policy.EffectAllow, nil
}
// Extract role name from principal ARN // Extract role name from principal ARN
roleName := utils.ExtractRoleNameFromPrincipal(request.Principal) roleName := utils.ExtractRoleNameFromPrincipal(request.Principal)
if roleName == "" { if roleName == "" {
@ -301,14 +322,6 @@ func (m *IAMManager) IsActionAllowed(ctx context.Context, request *ActionRequest
return false, fmt.Errorf("role not found: %s", roleName) return false, fmt.Errorf("role not found: %s", roleName)
} }
// Create evaluation context
evalCtx := &policy.EvaluationContext{
Principal: request.Principal,
Action: request.Action,
Resource: request.Resource,
RequestContext: request.RequestContext,
}
// Evaluate policies attached to the role // Evaluate policies attached to the role
result, err := m.policyEngine.Evaluate(ctx, "", evalCtx, roleDef.AttachedPolicies) result, err := m.policyEngine.Evaluate(ctx, "", evalCtx, roleDef.AttachedPolicies)
if err != nil { if err != nil {

2
weed/pb/filer_pb/filer.pb.go

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.6 // protoc-gen-go v1.36.6
// protoc v5.29.3
// protoc v6.33.1
// source: filer.proto // source: filer.proto
package filer_pb package filer_pb

2
weed/pb/filer_pb/filer_grpc.pb.go

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT. // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions: // versions:
// - protoc-gen-go-grpc v1.5.1 // - protoc-gen-go-grpc v1.5.1
// - protoc v5.29.3
// - protoc v6.33.1
// source: filer.proto // source: filer.proto
package filer_pb package filer_pb

1
weed/pb/iam.proto

@ -27,6 +27,7 @@ message Identity {
Account account = 4; Account account = 4;
bool disabled = 5; // User status: false = enabled (default), true = disabled bool disabled = 5; // User status: false = enabled (default), true = disabled
repeated string service_account_ids = 6; // IDs of service accounts owned by this user repeated string service_account_ids = 6; // IDs of service accounts owned by this user
repeated string policy_names = 7;
} }
message Credential { message Credential {

13
weed/pb/iam_pb/iam.pb.go

@ -89,6 +89,7 @@ type Identity struct {
Account *Account `protobuf:"bytes,4,opt,name=account,proto3" json:"account,omitempty"` Account *Account `protobuf:"bytes,4,opt,name=account,proto3" json:"account,omitempty"`
Disabled bool `protobuf:"varint,5,opt,name=disabled,proto3" json:"disabled,omitempty"` // User status: false = enabled (default), true = disabled Disabled bool `protobuf:"varint,5,opt,name=disabled,proto3" json:"disabled,omitempty"` // User status: false = enabled (default), true = disabled
ServiceAccountIds []string `protobuf:"bytes,6,rep,name=service_account_ids,json=serviceAccountIds,proto3" json:"service_account_ids,omitempty"` // IDs of service accounts owned by this user ServiceAccountIds []string `protobuf:"bytes,6,rep,name=service_account_ids,json=serviceAccountIds,proto3" json:"service_account_ids,omitempty"` // IDs of service accounts owned by this user
PolicyNames []string `protobuf:"bytes,7,rep,name=policy_names,json=policyNames,proto3" json:"policy_names,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@ -165,6 +166,13 @@ func (x *Identity) GetServiceAccountIds() []string {
return nil return nil
} }
func (x *Identity) GetPolicyNames() []string {
if x != nil {
return x.PolicyNames
}
return nil
}
type Credential struct { type Credential struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
AccessKey string `protobuf:"bytes,1,opt,name=access_key,json=accessKey,proto3" json:"access_key,omitempty"` AccessKey string `protobuf:"bytes,1,opt,name=access_key,json=accessKey,proto3" json:"access_key,omitempty"`
@ -405,14 +413,15 @@ const file_iam_proto_rawDesc = "" +
"identities\x18\x01 \x03(\v2\x10.iam_pb.IdentityR\n" + "identities\x18\x01 \x03(\v2\x10.iam_pb.IdentityR\n" +
"identities\x12+\n" + "identities\x12+\n" +
"\baccounts\x18\x02 \x03(\v2\x0f.iam_pb.AccountR\baccounts\x12A\n" + "\baccounts\x18\x02 \x03(\v2\x0f.iam_pb.AccountR\baccounts\x12A\n" +
"\x10service_accounts\x18\x03 \x03(\v2\x16.iam_pb.ServiceAccountR\x0fserviceAccounts\"\xe5\x01\n" +
"\x10service_accounts\x18\x03 \x03(\v2\x16.iam_pb.ServiceAccountR\x0fserviceAccounts\"\x88\x02\n" +
"\bIdentity\x12\x12\n" + "\bIdentity\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\x124\n" + "\x04name\x18\x01 \x01(\tR\x04name\x124\n" +
"\vcredentials\x18\x02 \x03(\v2\x12.iam_pb.CredentialR\vcredentials\x12\x18\n" + "\vcredentials\x18\x02 \x03(\v2\x12.iam_pb.CredentialR\vcredentials\x12\x18\n" +
"\aactions\x18\x03 \x03(\tR\aactions\x12)\n" + "\aactions\x18\x03 \x03(\tR\aactions\x12)\n" +
"\aaccount\x18\x04 \x01(\v2\x0f.iam_pb.AccountR\aaccount\x12\x1a\n" + "\aaccount\x18\x04 \x01(\v2\x0f.iam_pb.AccountR\aaccount\x12\x1a\n" +
"\bdisabled\x18\x05 \x01(\bR\bdisabled\x12.\n" + "\bdisabled\x18\x05 \x01(\bR\bdisabled\x12.\n" +
"\x13service_account_ids\x18\x06 \x03(\tR\x11serviceAccountIds\"b\n" +
"\x13service_account_ids\x18\x06 \x03(\tR\x11serviceAccountIds\x12!\n" +
"\fpolicy_names\x18\a \x03(\tR\vpolicyNames\"b\n" +
"\n" + "\n" +
"Credential\x12\x1d\n" + "Credential\x12\x1d\n" +
"\n" + "\n" +

2
weed/pb/master_pb/master.pb.go

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.6 // protoc-gen-go v1.36.6
// protoc v5.29.3
// protoc v6.33.1
// source: master.proto // source: master.proto
package master_pb package master_pb

2
weed/pb/master_pb/master_grpc.pb.go

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT. // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions: // versions:
// - protoc-gen-go-grpc v1.5.1 // - protoc-gen-go-grpc v1.5.1
// - protoc v5.29.3
// - protoc v6.33.1
// source: master.proto // source: master.proto
package master_pb package master_pb

2
weed/pb/mount_pb/mount.pb.go

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.6 // protoc-gen-go v1.36.6
// protoc v5.29.3
// protoc v6.33.1
// source: mount.proto // source: mount.proto
package mount_pb package mount_pb

2
weed/pb/mount_pb/mount_grpc.pb.go

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT. // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions: // versions:
// - protoc-gen-go-grpc v1.5.1 // - protoc-gen-go-grpc v1.5.1
// - protoc v5.29.3
// - protoc v6.33.1
// source: mount.proto // source: mount.proto
package mount_pb package mount_pb

2
weed/pb/mq_agent_pb/mq_agent.pb.go

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.6 // protoc-gen-go v1.36.6
// protoc v5.29.3
// protoc v6.33.1
// source: mq_agent.proto // source: mq_agent.proto
package mq_agent_pb package mq_agent_pb

2
weed/pb/mq_agent_pb/mq_agent_grpc.pb.go

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT. // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions: // versions:
// - protoc-gen-go-grpc v1.5.1 // - protoc-gen-go-grpc v1.5.1
// - protoc v5.29.3
// - protoc v6.33.1
// source: mq_agent.proto // source: mq_agent.proto
package mq_agent_pb package mq_agent_pb

2
weed/pb/mq_pb/mq_broker.pb.go

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.6 // protoc-gen-go v1.36.6
// protoc v5.29.3
// protoc v6.33.1
// source: mq_broker.proto // source: mq_broker.proto
package mq_pb package mq_pb

2
weed/pb/mq_pb/mq_broker_grpc.pb.go

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT. // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions: // versions:
// - protoc-gen-go-grpc v1.5.1 // - protoc-gen-go-grpc v1.5.1
// - protoc v5.29.3
// - protoc v6.33.1
// source: mq_broker.proto // source: mq_broker.proto
package mq_pb package mq_pb

2
weed/pb/remote_pb/remote.pb.go

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.6 // protoc-gen-go v1.36.6
// protoc v5.29.3
// protoc v6.33.1
// source: remote.proto // source: remote.proto
package remote_pb package remote_pb

2
weed/pb/s3_pb/s3.pb.go

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.6 // protoc-gen-go v1.36.6
// protoc v5.29.3
// protoc v6.33.1
// source: s3.proto // source: s3.proto
package s3_pb package s3_pb

2
weed/pb/s3_pb/s3_grpc.pb.go

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT. // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions: // versions:
// - protoc-gen-go-grpc v1.5.1 // - protoc-gen-go-grpc v1.5.1
// - protoc v5.29.3
// - protoc v6.33.1
// source: s3.proto // source: s3.proto
package s3_pb package s3_pb

2
weed/pb/schema_pb/mq_schema.pb.go

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.6 // protoc-gen-go v1.36.6
// protoc v5.29.3
// protoc v6.33.1
// source: mq_schema.proto // source: mq_schema.proto
package schema_pb package schema_pb

2
weed/pb/volume_server_pb/volume_server.pb.go

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.6 // protoc-gen-go v1.36.6
// protoc v5.29.3
// protoc v6.33.1
// source: volume_server.proto // source: volume_server.proto
package volume_server_pb package volume_server_pb

2
weed/pb/volume_server_pb/volume_server_grpc.pb.go

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT. // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions: // versions:
// - protoc-gen-go-grpc v1.5.1 // - protoc-gen-go-grpc v1.5.1
// - protoc v5.29.3
// - protoc v6.33.1
// source: volume_server.proto // source: volume_server.proto
package volume_server_pb package volume_server_pb

2
weed/pb/worker_pb/worker.pb.go

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.6 // protoc-gen-go v1.36.6
// protoc v5.29.3
// protoc v6.33.1
// source: worker.proto // source: worker.proto
package worker_pb package worker_pb

2
weed/pb/worker_pb/worker_grpc.pb.go

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT. // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions: // versions:
// - protoc-gen-go-grpc v1.5.1 // - protoc-gen-go-grpc v1.5.1
// - protoc v5.29.3
// - protoc v6.33.1
// source: worker.proto // source: worker.proto
package worker_pb package worker_pb

18
weed/s3api/auth_credentials.go

@ -66,8 +66,9 @@ type Identity struct {
Account *Account Account *Account
Credentials []*Credential Credentials []*Credential
Actions []Action Actions []Action
PrincipalArn string // ARN for IAM authorization (e.g., "arn:aws:iam::account-id:user/username")
Disabled bool // User status: false = enabled (default), true = disabled
PolicyNames []string // Attached IAM policy names
PrincipalArn string // ARN for IAM authorization (e.g., "arn:aws:iam::account-id:user/username")
Disabled bool // User status: false = enabled (default), true = disabled
} }
// Account represents a system user, a system user can // Account represents a system user, a system user can
@ -310,6 +311,7 @@ func (iam *IdentityAccessManagement) loadS3ApiConfiguration(config *iam_pb.S3Api
Actions: nil, Actions: nil,
PrincipalArn: generatePrincipalArn(ident.Name), PrincipalArn: generatePrincipalArn(ident.Name),
Disabled: ident.Disabled, // false (default) = enabled, true = disabled Disabled: ident.Disabled, // false (default) = enabled, true = disabled
PolicyNames: ident.PolicyNames,
} }
switch { switch {
case ident.Name == AccountAnonymous.Id: case ident.Name == AccountAnonymous.Id:
@ -939,9 +941,10 @@ func (iam *IdentityAccessManagement) authenticateJWTWithIAM(r *http.Request) (*I
// Convert IAMIdentity to existing Identity structure // Convert IAMIdentity to existing Identity structure
identity := &Identity{ identity := &Identity{
Name: iamIdentity.Name,
Account: iamIdentity.Account,
Actions: []Action{}, // Empty - authorization handled by policy engine
Name: iamIdentity.Name,
Account: iamIdentity.Account,
Actions: []Action{}, // Empty - authorization handled by policy engine
PolicyNames: iamIdentity.PolicyNames,
} }
// Store session info in request headers for later authorization // Store session info in request headers for later authorization
@ -997,8 +1000,9 @@ func (iam *IdentityAccessManagement) authorizeWithIAM(r *http.Request, identity
// Create IAMIdentity for authorization // Create IAMIdentity for authorization
iamIdentity := &IAMIdentity{ iamIdentity := &IAMIdentity{
Name: identity.Name,
Account: identity.Account,
Name: identity.Name,
Account: identity.Account,
PolicyNames: identity.PolicyNames,
} }
// Determine authorization path and configure identity // Determine authorization path and configure identity

5
weed/s3api/s3_iam_middleware.go

@ -193,6 +193,7 @@ func (s3iam *S3IAMIntegration) AuthorizeAction(ctx context.Context, identity *IA
Resource: resourceArn, Resource: resourceArn,
SessionToken: identity.SessionToken, SessionToken: identity.SessionToken,
RequestContext: requestContext, RequestContext: requestContext,
PolicyNames: identity.PolicyNames,
} }
// Check if action is allowed using our policy engine // Check if action is allowed using our policy engine
@ -214,6 +215,7 @@ type IAMIdentity struct {
Principal string Principal string
SessionToken string SessionToken string
Account *Account Account *Account
PolicyNames []string
} }
// IsAdmin checks if the identity has admin privileges // IsAdmin checks if the identity has admin privileges
@ -490,7 +492,8 @@ func (enhanced *EnhancedS3ApiServer) AuthenticateJWTRequest(r *http.Request) (*I
Name: iamIdentity.Name, Name: iamIdentity.Name,
Account: iamIdentity.Account, Account: iamIdentity.Account,
// Note: Actions will be determined by policy evaluation // Note: Actions will be determined by policy evaluation
Actions: []Action{}, // Empty - authorization handled by policy engine
Actions: []Action{}, // Empty - authorization handled by policy engine
PolicyNames: iamIdentity.PolicyNames,
} }
// Store session token for later authorization // Store session token for later authorization

Loading…
Cancel
Save