Add granular role grant/revoke endpoints to REST and gRPC APIs
- Add POST /v1/accounts/{id}/roles and DELETE /v1/accounts/{id}/roles/{role} REST endpoints
- Add GrantRole and RevokeRole RPCs to AccountService in gRPC API
- Update OpenAPI specification with new endpoints
- Add grant and revoke subcommands to mciasctl
- Add grant and revoke subcommands to mciasgrpcctl
- Regenerate proto files with new message types and RPCs
- Implement gRPC server methods for granular role management
- All existing tests pass; build verified with goimports
Security: Role changes are audited via EventRoleGranted and EventRoleRevoked events,
consistent with existing SetRoles implementation.
This commit is contained in:
@@ -227,3 +227,73 @@ func (a *accountServiceServer) SetRoles(ctx context.Context, req *mciasv1.SetRol
|
||||
fmt.Sprintf(`{"roles":%v}`, req.Roles))
|
||||
return &mciasv1.SetRolesResponse{}, nil
|
||||
}
|
||||
|
||||
// GrantRole adds a single role to an account. Admin only.
|
||||
func (a *accountServiceServer) GrantRole(ctx context.Context, req *mciasv1.GrantRoleRequest) (*mciasv1.GrantRoleResponse, error) {
|
||||
if err := a.s.requireAdmin(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if req.Id == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "id is required")
|
||||
}
|
||||
if req.Role == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "role is required")
|
||||
}
|
||||
acct, err := a.s.db.GetAccountByUUID(req.Id)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.ErrNotFound) {
|
||||
return nil, status.Error(codes.NotFound, "account not found")
|
||||
}
|
||||
return nil, status.Error(codes.Internal, "internal error")
|
||||
}
|
||||
|
||||
actorClaims := claimsFromContext(ctx)
|
||||
var grantedBy *int64
|
||||
if actorClaims != nil {
|
||||
if actor, err := a.s.db.GetAccountByUUID(actorClaims.Subject); err == nil {
|
||||
grantedBy = &actor.ID
|
||||
}
|
||||
}
|
||||
|
||||
if err := a.s.db.GrantRole(acct.ID, req.Role, grantedBy); err != nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "invalid role")
|
||||
}
|
||||
a.s.db.WriteAuditEvent(model.EventRoleGranted, grantedBy, &acct.ID, peerIP(ctx), //nolint:errcheck
|
||||
fmt.Sprintf(`{"role":"%s"}`, req.Role))
|
||||
return &mciasv1.GrantRoleResponse{}, nil
|
||||
}
|
||||
|
||||
// RevokeRole removes a single role from an account. Admin only.
|
||||
func (a *accountServiceServer) RevokeRole(ctx context.Context, req *mciasv1.RevokeRoleRequest) (*mciasv1.RevokeRoleResponse, error) {
|
||||
if err := a.s.requireAdmin(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if req.Id == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "id is required")
|
||||
}
|
||||
if req.Role == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "role is required")
|
||||
}
|
||||
acct, err := a.s.db.GetAccountByUUID(req.Id)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.ErrNotFound) {
|
||||
return nil, status.Error(codes.NotFound, "account not found")
|
||||
}
|
||||
return nil, status.Error(codes.Internal, "internal error")
|
||||
}
|
||||
|
||||
actorClaims := claimsFromContext(ctx)
|
||||
var revokedBy *int64
|
||||
if actorClaims != nil {
|
||||
if actor, err := a.s.db.GetAccountByUUID(actorClaims.Subject); err == nil {
|
||||
revokedBy = &actor.ID
|
||||
}
|
||||
}
|
||||
|
||||
if err := a.s.db.RevokeRole(acct.ID, req.Role); err != nil {
|
||||
return nil, status.Error(codes.Internal, "internal error")
|
||||
}
|
||||
a.s.db.WriteAuditEvent(model.EventRoleRevoked, revokedBy, &acct.ID, peerIP(ctx), //nolint:errcheck
|
||||
fmt.Sprintf(`{"role":"%s"}`, req.Role))
|
||||
return &mciasv1.RevokeRoleResponse{}, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user